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.
Photo by Markus Spiske on Unsplash
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.
Like what you're reading? Subscribe to DevTools Guide — free weekly guides in your inbox.
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.
