| name | shadcn-ui |
| description | Builds accessible UI with shadcn/ui including component installation, customization, theming, and Radix primitives. Use when adding UI components, customizing design systems, implementing accessible interfaces, or building component libraries. |
shadcn/ui
Beautifully designed, accessible, and customizable React components built on Radix UI primitives.
Quick Start
Initialize in project:
npx shadcn@latest init
Configuration prompts:
Would you like to use TypeScript? yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? app/globals.css
Would you like to use CSS variables? yes
Where is your tailwind.config.js? tailwind.config.ts
Configure the import alias for components? @/components
Configure the import alias for utils? @/lib/utils
Add components:
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add form
Project Structure
src/
components/
ui/ # shadcn/ui components
button.tsx
card.tsx
input.tsx
lib/
utils.ts # cn() utility
app/
globals.css # CSS variables
Core Utility
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Button
npx shadcn@latest add button
import { Button } from '@/components/ui/button';
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><IconPlus /></Button>
// States
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/about">As Link</Link>
</Button>
// With loading state
<Button disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit
</Button>
Card
npx shadcn@latest add card
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here.</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
Form
npx shadcn@latest add form input label
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
});
type FormValues = z.infer<typeof formSchema>;
function ProfileForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
email: '',
},
});
function onSubmit(values: FormValues) {
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
Your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Dialog
npx shadcn@latest add dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from '@/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input placeholder="Name" />
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Controlled Dialog
function ControlledDialog() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button
onClick={() => {
handleAction();
setOpen(false);
}}
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Select
npx shadcn@latest add select
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
// Controlled
const [value, setValue] = useState('');
<Select value={value} onValueChange={setValue}>
{/* ... */}
</Select>
DropdownMenu
npx shadcn@latest add dropdown-menu
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleEdit()}>
Edit
<DropdownMenuShortcut>Cmd+E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDuplicate()}>
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDelete()}
>
Delete
<DropdownMenuShortcut>Cmd+Delete</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Table
npx shadcn@latest add table
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
<Table>
<TableCaption>A list of recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">{invoice.id}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
Toast
npx shadcn@latest add toast
// In layout
import { Toaster } from '@/components/ui/toaster';
export default function Layout({ children }) {
return (
<>
{children}
<Toaster />
</>
);
}
// Usage
import { useToast } from '@/components/ui/use-toast';
function MyComponent() {
const { toast } = useToast();
return (
<Button
onClick={() => {
toast({
title: 'Success!',
description: 'Your action was completed.',
});
}}
>
Show Toast
</Button>
);
}
// Variants
toast({
variant: 'destructive',
title: 'Error',
description: 'Something went wrong.',
});
toast({
title: 'Scheduled',
description: 'Event scheduled for Friday.',
action: (
<ToastAction altText="Undo">Undo</ToastAction>
),
});
Alert Dialog
npx shadcn@latest add alert-dialog
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>
Yes, delete account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Theming
CSS Variables
/* globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... other dark mode values */
}
}
Custom Colors
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: {
DEFAULT: 'hsl(var(--brand))',
foreground: 'hsl(var(--brand-foreground))',
},
},
},
},
};
/* globals.css */
:root {
--brand: 262.1 83.3% 57.8%;
--brand-foreground: 210 40% 98%;
}
Customizing Components
Extending Variants
// components/ui/button.tsx
const buttonVariants = cva(
'inline-flex items-center justify-center...',
{
variants: {
variant: {
default: '...',
destructive: '...',
// Add custom variant
success:
'bg-green-500 text-white hover:bg-green-600',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
// Add custom size
xl: 'h-14 rounded-lg px-10 text-lg',
},
},
}
);
// Usage
<Button variant="success" size="xl">Big Success</Button>
Creating Compound Components
// components/stat-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
interface StatCardProps {
title: string;
value: string | number;
description?: string;
trend?: 'up' | 'down';
className?: string;
}
export function StatCard({
title,
value,
description,
trend,
className,
}: StatCardProps) {
return (
<Card className={cn('', className)}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p
className={cn(
'text-xs',
trend === 'up' && 'text-green-600',
trend === 'down' && 'text-red-600',
!trend && 'text-muted-foreground'
)}
>
{description}
</p>
)}
</CardContent>
</Card>
);
}
Dark Mode
// components/theme-toggle.tsx
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { setTheme, theme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}
Best Practices
- Copy, don't import - Own your components
- Use CSS variables - For consistent theming
- Leverage cn() - For conditional classes
- Keep accessibility - Don't remove ARIA attributes
- Customize locally - Modify copied components as needed
Common Mistakes
| Mistake | Fix |
|---|---|
| Importing from npm | Use local components |
| Removing accessibility | Keep Radix primitives |
| Over-customizing | Start with defaults |
| Ignoring dark mode | Test both themes |
| Hardcoded colors | Use CSS variables |
Reference Files
- references/components.md - All component patterns
- references/theming.md - Advanced theming
- references/accessibility.md - A11y patterns