Skip to content

Getting started

Installation

Components are copied into your project with the shadcn CLI, so you own every line. Set up the shared pieces once, then add what you need.

Requirements

React 19, Tailwind CSS v4 and TypeScript. Works anywhere the shadcn CLI does — Next.js, Vite, React Router, TanStack Start.

Initialize shadcn

Skip this if your project already has a components.json.

pnpm dlx shadcn@latest init

Add a component

Each component pulls in its npm dependencies, the cn helper and the theme tokens.

pnpm dlx shadcn@latest add https://tuffui.com/r/tool-approval-queue.json

Theme tokens

Components use your existing background, foreground and muted-foreground, plus a few additive tokens. They install automatically; to add them on their own:

pnpm dlx shadcn@latest add https://tuffui.com/r/theme.json

Manual setup

Not using the CLI? Install the dependencies, add the tokens to your global CSS and create the cn helper.

pnpm add motion lucide-react clsx tailwind-merge
app/globals.css
:root {
  --surface: oklch(0.948 0.002 286);
  --surface-inset: oklch(1 0 0);
  --surface-raised: oklch(0.912 0.003 286);
  --brand: #0071e3;
  --brand-foreground: #ffffff;
  --positive: #248a3d;
  --negative: #d70015;
  --warning: #c93400;
  --button: linear-gradient(0deg, #fff 0%, #e6e6e6 100%);
  --button-hover: linear-gradient(0deg, #fff 0%, #eeeeee 100%);
  --button-foreground: #111111;
  --button-edge: rgba(0, 0, 0, 0.1);
  --button-shadow: 0 0 #0000;
  --button-secondary: linear-gradient(180deg, #e2e2e2 0%, #ececec 100%);
  --button-secondary-hover: linear-gradient(180deg, #dadada 0%, #e4e4e4 100%);
}

.dark {
  --surface: oklch(0.212 0 0);
  --surface-inset: oklch(0.172 0 0);
  --surface-raised: oklch(0.265 0 0);
  --brand: #0a84ff;
  --brand-foreground: #ffffff;
  --positive: #30d158;
  --negative: #ff453a;
  --warning: #ff9f0a;
  --button: linear-gradient(0deg, #fff 0%, #ababab 100%);
  --button-hover: linear-gradient(0deg, #fff 0%, #c2c2c2 100%);
  --button-foreground: #111111;
  --button-edge: rgba(255, 255, 255, 0.5);
  --button-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
  --button-secondary: linear-gradient(180deg, #1d1d1d 0%, #262626 100%);
  --button-secondary-hover: linear-gradient(180deg, #242424 0%, #2d2d2d 100%);
}

@theme inline {
  --color-surface: var(--surface);
  --color-surface-inset: var(--surface-inset);
  --color-surface-raised: var(--surface-raised);
  --color-brand: var(--brand);
  --color-brand-foreground: var(--brand-foreground);
  --color-positive: var(--positive);
  --color-negative: var(--negative);
  --color-warning: var(--warning);
}
lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Motion

Animations use motion (formerly Framer Motion) with spring physics. Every component wraps itself in MotionConfig reducedMotion="user", so transforms and layout animations turn off when the OS asks for reduced motion — opacity changes stay so state is still visible.

# Demos and previews are driven by a scripted run — handy for your own tests too.
pnpm dlx shadcn@latest add https://tuffui.com/r/use-mock-run.json