shadcn/ui: Copy-Paste Component Library for React and Tailwind
Most UI libraries ship as npm packages — you install them, use their components, and live with their constraints. shadcn/ui takes a different approach: it's a collection of components you copy directly into your codebase. No package to install, no version upgrades, no API constraints. You own the code.
This makes shadcn/ui unusually adaptable. Every component is a starting point, not a black box.
The Model: Copy-Paste, Not Install
Traditional library:
npm install @some-ui/button
# Use it, but can't modify internals without forking
shadcn/ui:
npx shadcn@latest add button
# Creates src/components/ui/button.tsx in your project
# It's your code now — read it, modify it, own it
Each component is built on:
- Radix UI — headless, accessible primitives (handles ARIA, focus management, keyboard nav)
- Tailwind CSS — for styling
- class-variance-authority (cva) — for component variants
- clsx / tailwind-merge — for conditional class names
Installation
Prerequisites: React project with Tailwind CSS configured.
# Initialize shadcn/ui in your project
npx shadcn@latest init
The init command asks about your project structure:
- TypeScript or JavaScript
- Style (Default or New York — different visual aesthetics)
- Base color (Slate, Gray, Zinc, Neutral, Stone)
- CSS variables for colors (recommended)
- Where to put components (
src/components/uiby default)
It adds dependencies (radix-ui packages, clsx, tailwind-merge, class-variance-authority) and configures tailwind.config.js.
Adding Components
Add components individually as you need them:
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add select
npx shadcn@latest add table
Or add multiple at once:
npx shadcn@latest add button input label form
Each add command creates one or more files in your components/ui/ directory. These are your files — commit them, modify them, and don't treat them as sacred.
Using Components
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export function LoginForm() {
return (
<form className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="[email protected]" />
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
<Button type="submit" className="w-full">Sign in</Button>
</form>
);
}
Component Variants
shadcn/ui components use cva (class-variance-authority) for variants. The Button component, for example:
// src/components/ui/button.tsx (your file — look at it!)
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground ...",
outline: "border border-input bg-background ...",
secondary: "bg-secondary text-secondary-foreground ...",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
Usage:
<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline" size="sm">Small</Button>
<Button variant="ghost" size="icon"><Icon /></Button>
The Form Component
shadcn/ui's Form component wraps React Hook Form with Zod validation, providing accessible form fields with error messages:
npx shadcn@latest add form
npm install react-hook-form @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form, FormField, FormItem, FormLabel,
FormControl, FormMessage
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type FormData = z.infer<typeof schema>;
export function LoginForm() {
const form = useForm<FormData>({ resolver: zodResolver(schema) });
function onSubmit(data: FormData) {
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Sign in</Button>
</form>
</Form>
);
}
Theming with CSS Variables
shadcn/ui uses CSS custom properties for theming. The default globals.css includes variables for light and dark mode:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... more variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode overrides */
}
To change your brand color, update --primary and --primary-foreground. The change propagates across all components that use those variables.
Use the shadcn/ui themes generator to visually select and export a color scheme.
Dark Mode
Add dark mode support with next-themes (or any class-based dark mode solution):
npm install next-themes
// app/providers.tsx
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
// components/theme-toggle.tsx
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-5 w-5 rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
</Button>
);
}
Modifying Components
This is where shadcn/ui shines. Because components live in your codebase, you can change anything:
// src/components/ui/button.tsx
// Add a new variant:
const buttonVariants = cva("...", {
variants: {
variant: {
// ... existing variants ...
brand: "bg-purple-600 text-white hover:bg-purple-700", // your addition
},
},
});
// Usage:
<Button variant="brand">Upgrade Now</Button>
You can also create new components that compose existing ones:
// src/components/ui/icon-button.tsx
import { Button, ButtonProps } from './button';
import { LucideIcon } from 'lucide-react';
interface IconButtonProps extends ButtonProps {
icon: LucideIcon;
label: string;
}
export function IconButton({ icon: Icon, label, ...props }: IconButtonProps) {
return (
<Button {...props}>
<Icon className="mr-2 h-4 w-4" />
{label}
</Button>
);
}
Accessibility Out of the Box
Radix UI (the underlying primitives) handles ARIA attributes, focus trapping, keyboard navigation, and screen reader support for complex components:
- Dialog/AlertDialog: Focus trap, ESC to close, aria-modal
- Select/Combobox: Keyboard navigation, aria-expanded, aria-selected
- Checkbox/Switch: Proper role and checked state
- Tooltip: Proper ARIA relationship between trigger and tooltip
You get accessibility without thinking about it — unless you modify components in ways that break the Radix primitives' guarantees.
shadcn/ui vs Alternatives
| Library | Approach | Customization | Bundle size |
|---|---|---|---|
| shadcn/ui | Copy-paste | Full — you own it | Zero (no npm dep) |
| Mantine | npm package | Config + CSS vars | ~200KB |
| Chakra UI | npm package | Theme system | ~170KB |
| Ant Design | npm package | Theme tokens | ~500KB+ |
| Headless UI | npm package | Full (no styles) | ~20KB |
Summary
shadcn/ui is the right default component library for new React + Tailwind projects. The copy-paste model means you get beautiful, accessible components as a starting point — not a dependency you're locked into.
Initialize it with npx shadcn@latest init, add components as you need them, and treat the generated files as your own code from day one.