shadcn/ui Skill
shadcn/ui is a collection of beautifully-designed, accessible components and a code distribution platform built with TypeScript, Tailwind CSS, and Radix UI primitives. It's not a traditional component library but a collection of reusable components you can copy and paste into your apps.
Reference
https://ui.shadcn.com/llms.txt
When to Use This Skill
Use this skill when:
-
Building user interfaces with React-based frameworks (Next.js, Vite, Remix, Astro, etc.)
-
Adding pre-built, accessible UI components to applications
-
Implementing design systems with Tailwind CSS
-
Setting up forms with validation (React Hook Form + Zod)
-
Adding data tables, charts, or complex UI patterns
-
Implementing dark mode with consistent theming
-
Customizing component appearance and behavior
Core Concepts
Key Principles
-
Open Code: Copy components into your project, modify freely
-
Composition: Built with composable primitives from Radix UI
-
Distribution: Components distributed via CLI, not npm packages
-
Beautiful Defaults: Thoughtfully designed with excellent aesthetics
-
AI-Ready: Structured for easy integration with AI tools
Architecture
shadcn/ui follows a unique distribution model:
-
CLI Tool: Installs and manages components via npx shadcn@latest
-
Component Registry: Central repository of components
-
Local Components: Components live in your components/ui/ directory
-
Full Ownership: You own the code, modify as needed
Technology Stack
-
TypeScript: Full type safety
-
Tailwind CSS: Utility-first styling (v3 and v4 support)
-
Radix UI: Accessible, unstyled primitives
-
Class Variance Authority: Component variants
-
React 19: Compatible with latest React
Installation & Setup
Initial Setup
Using the CLI (Recommended):
npx shadcn@latest init
The CLI will prompt for:
-
Framework preference (Next.js, Vite, etc.)
-
TypeScript or JavaScript
-
Component installation location
-
CSS variables or Tailwind configuration
-
Color theme preferences
-
Global CSS file location
Manual Setup:
- Install dependencies:
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
- Create components.json :
{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "zinc", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } }
- Configure Tailwind:
// tailwind.config.ts import type { Config } from "tailwindcss"
const config: Config = { darkMode: ["class"], content: [ './pages//*.{ts,tsx}', './components//.{ts,tsx}', './app/**/.{ts,tsx}', ], theme: { extend: {}, }, plugins: [require("tailwindcss-animate")], }
export default config
- Create utility file:
// lib/utils.ts import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
Adding Components
Via CLI:
Add single component
npx shadcn@latest add button
Add multiple components
npx shadcn@latest add button card dialog
Add all components
npx shadcn@latest add --all
What happens when you add a component:
-
Component files are copied to components/ui/
-
Dependencies are automatically installed
-
Component is ready to import and use
Component Categories
Form & Input Components
Button:
import { Button } from "@/components/ui/button"
<Button variant="default">Click me</Button> <Button variant="destructive">Delete</Button> <Button variant="outline" size="sm">Small</Button> <Button variant="ghost" size="icon"> <Icon /> </Button>
Input:
import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label"
<div> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="you@example.com" /> </div>
Form (with validation):
import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button"
const formSchema = z.object({ username: z.string().min(2).max(50), email: z.string().email(), })
function ProfileForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { username: "", email: "", }, })
function onSubmit(values: z.infer<typeof formSchema>) { console.log(values) }
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <FormField control={form.control} name="username" render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" {...field} /> </FormControl> <FormDescription> This is your public display name. </FormDescription> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> ) }
Select:
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"
<Select> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Theme" /> </SelectTrigger> <SelectContent> <SelectItem value="light">Light</SelectItem> <SelectItem value="dark">Dark</SelectItem> <SelectItem value="system">System</SelectItem> </SelectContent> </Select>
Checkbox:
import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label"
<div className="flex items-center space-x-2"> <Checkbox id="terms" /> <Label htmlFor="terms">Accept terms and conditions</Label> </div>
Date Picker:
import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { CalendarIcon } from "lucide-react" import { format } from "date-fns"
const [date, setDate] = useState<Date>()
<Popover> <PopoverTrigger asChild> <Button variant="outline"> <CalendarIcon className="mr-2 h-4 w-4" /> {date ? format(date, "PPP") : "Pick a date"} </Button> </PopoverTrigger> <PopoverContent className="w-auto p-0"> <Calendar mode="single" selected={date} onSelect={setDate} /> </PopoverContent> </Popover>
Layout & Navigation
Card:
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"
<Card> <CardHeader> <CardTitle>Card Title</CardTitle> <CardDescription>Card Description</CardDescription> </CardHeader> <CardContent> <p>Card Content</p> </CardContent> <CardFooter> <p>Card Footer</p> </CardFooter> </Card>
Tabs:
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="account"> <TabsList> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContent value="account">Account settings</TabsContent> <TabsContent value="password">Password settings</TabsContent> </Tabs>
Accordion:
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"
<Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>Is it accessible?</AccordionTrigger> <AccordionContent> Yes. It adheres to the WAI-ARIA design pattern. </AccordionContent> </AccordionItem> </Accordion>
Navigation Menu:
import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, } from "@/components/ui/navigation-menu"
<NavigationMenu> <NavigationMenuList> <NavigationMenuItem> <NavigationMenuTrigger>Item One</NavigationMenuTrigger> <NavigationMenuContent> <NavigationMenuLink>Link</NavigationMenuLink> </NavigationMenuContent> </NavigationMenuItem> </NavigationMenuList> </NavigationMenu>
Overlays & Dialogs
Dialog:
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"
<Dialog> <DialogTrigger asChild> <Button>Open Dialog</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Are you sure?</DialogTitle> <DialogDescription> This action cannot be undone. </DialogDescription> </DialogHeader> </DialogContent> </Dialog>
Drawer:
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"
<Drawer> <DrawerTrigger>Open</DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Are you sure?</DrawerTitle> <DrawerDescription>This action cannot be undone.</DrawerDescription> </DrawerHeader> <DrawerFooter> <Button>Submit</Button> <DrawerClose>Cancel</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer>
Popover:
import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"
<Popover> <PopoverTrigger>Open</PopoverTrigger> <PopoverContent>Place content here.</PopoverContent> </Popover>
Toast:
import { useToast } from "@/hooks/use-toast" import { Button } from "@/components/ui/button"
const { toast } = useToast()
<Button onClick={() => { toast({ title: "Scheduled: Catch up", description: "Friday, February 10, 2023 at 5:57 PM", }) }}
Show Toast </Button>
Command:
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"
<Command> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search Emoji</CommandItem> <CommandItem>Calculator</CommandItem> </CommandGroup> </CommandList> </Command>
Feedback & Status
Alert:
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
<Alert> <AlertTitle>Heads up!</AlertTitle> <AlertDescription> You can add components to your app using the CLI. </AlertDescription> </Alert>
<Alert variant="destructive"> <AlertTitle>Error</AlertTitle> <AlertDescription> Your session has expired. Please log in again. </AlertDescription> </Alert>
Progress:
import { Progress } from "@/components/ui/progress"
<Progress value={33} />
Skeleton:
import { Skeleton } from "@/components/ui/skeleton"
<div className="flex items-center space-x-4"> <Skeleton className="h-12 w-12 rounded-full" /> <div className="space-y-2"> <Skeleton className="h-4 w-[250px]" /> <Skeleton className="h-4 w-[200px]" /> </div> </div>
Display Components
Table:
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"
<Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead>Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Amount</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> <TableCell>INV001</TableCell> <TableCell>Paid</TableCell> <TableCell>$250.00</TableCell> </TableRow> </TableBody> </Table>
Data Table (with sorting/filtering):
npx shadcn@latest add data-table
Avatar:
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar> <AvatarImage src="https://github.com/shadcn.png" /> <AvatarFallback>CN</AvatarFallback> </Avatar>
Badge:
import { Badge } from "@/components/ui/badge"
<Badge>Default</Badge> <Badge variant="secondary">Secondary</Badge> <Badge variant="destructive">Destructive</Badge> <Badge variant="outline">Outline</Badge>
Theming & Customization
Dark Mode Setup
Next.js (App Router):
- Install next-themes:
npm install next-themes
- Create theme provider:
// components/theme-provider.tsx "use client"
import * as React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }
- Wrap app with provider:
// app/layout.tsx import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }) { return ( <html lang="en" suppressHydrationWarning> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> ) }
- Add theme toggle:
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-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> ) }
Color Customization
Using CSS Variables:
/* globals.css */ @layer base { :root { --background: 0 0% 100%; --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%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; /* ... */ } }
Using Tailwind Config:
// tailwind.config.ts export default { theme: { extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, // ... }, }, }, }
Component Customization
Since components live in your codebase, you can modify them directly:
// components/ui/button.tsx // Modify variants, add new ones, change styles const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium", { variants: { variant: { default: "bg-primary text-primary-foreground", destructive: "bg-destructive text-destructive-foreground", outline: "border border-input bg-background", // Add custom variant custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white", }, 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-md px-10 text-lg", }, }, defaultVariants: { variant: "default", size: "default", }, } )
Advanced Patterns
Server Actions (Next.js)
// app/actions.ts "use server"
import { z } from "zod"
const schema = z.object({ email: z.string().email(), password: z.string().min(8), })
export async function createUser(formData: FormData) { const validatedFields = schema.safeParse({ email: formData.get("email"), password: formData.get("password"), })
if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, } }
// Create user }
// app/signup/page.tsx import { createUser } from "@/app/actions" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input"
export default function SignupPage() { return ( <form action={createUser}> <Input name="email" type="email" /> <Input name="password" type="password" /> <Button type="submit">Sign up</Button> </form> ) }
Reusable Form Patterns
// lib/form-utils.ts import { UseFormReturn } from "react-hook-form" import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" import { Input } from "@/components/ui/input"
export function TextFormField({ form, name, label, placeholder, type = "text", }: { form: UseFormReturn<any> name: string label: string placeholder?: string type?: string }) { return ( <FormField control={form.control} name={name} render={({ field }) => ( <FormItem> <FormLabel>{label}</FormLabel> <FormControl> <Input type={type} placeholder={placeholder} {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> ) }
Responsive Component Composition
import { useMediaQuery } from "@/hooks/use-media-query" import { Dialog, DialogContent } from "@/components/ui/dialog" import { Drawer, DrawerContent } from "@/components/ui/drawer"
export function ResponsiveDialog({ children, ...props }) { const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) { return ( <Dialog {...props}> <DialogContent>{children}</DialogContent> </Dialog> ) }
return ( <Drawer {...props}> <DrawerContent>{children}</DrawerContent> </Drawer> ) }
Best Practices
-
Use TypeScript: Leverage full type safety for better DX
-
Customize Components: Modify components directly in your codebase
-
Compose Primitives: Build complex UIs by composing simple components
-
Follow Accessibility: Components are built on accessible Radix UI primitives
-
Use Form Validation: Integrate React Hook Form + Zod for robust forms
-
Dark Mode: Implement theme switching for better UX
-
Responsive Design: Use Tailwind responsive utilities
-
Performance: Use code splitting and lazy loading for large component sets
-
Consistent Spacing: Use Tailwind spacing scale consistently
-
Icon Library: Use lucide-react for consistent icons
Framework-Specific Setup
Next.js
-
Support for App Router and Pages Router
-
Server Components compatibility
-
Server Actions integration
Vite
-
Fast development with HMR
-
Easy setup with TypeScript
Remix
-
Route-based architecture
-
Progressive enhancement
Astro
-
Static site generation
-
Islands architecture
Laravel (Inertia.js)
-
Backend integration with Laravel
-
React frontend with Inertia
Common Patterns
Loading States
import { Skeleton } from "@/components/ui/skeleton"
export function UserCardSkeleton() { return ( <div className="flex items-center space-x-4"> <Skeleton className="h-12 w-12 rounded-full" /> <div className="space-y-2"> <Skeleton className="h-4 w-[250px]" /> <Skeleton className="h-4 w-[200px]" /> </div> </div> ) }
export function UserCard({ user }: { user?: User }) { if (!user) return <UserCardSkeleton />
return ( <div className="flex items-center space-x-4"> <Avatar> <AvatarImage src={user.avatar} /> <AvatarFallback>{user.initials}</AvatarFallback> </Avatar> <div> <p className="text-sm font-medium">{user.name}</p> <p className="text-sm text-muted-foreground">{user.email}</p> </div> </div> ) }
Error Handling
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { AlertCircle } from "lucide-react"
export function ErrorAlert({ error }: { error: Error }) { return ( <Alert variant="destructive"> <AlertCircle className="h-4 w-4" /> <AlertTitle>Error</AlertTitle> <AlertDescription>{error.message}</AlertDescription> </Alert> ) }
Confirmation Dialogs
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"
export function DeleteConfirmation({ onConfirm }: { onConfirm: () => void }) { return ( <AlertDialog> <AlertDialogTrigger asChild> <Button variant="destructive">Delete</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={onConfirm}>Continue</AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ) }
Troubleshooting
Common Issues
"Module not found" errors
-
Check path aliases in tsconfig.json
-
Verify components.json aliases match your project structure
-
Ensure components are installed in correct directory
Styling not applied
-
Verify Tailwind CSS is configured correctly
-
Check globals.css imports CSS variables
-
Ensure tailwindcss-animate plugin is installed
Dark mode not working
-
Check ThemeProvider is wrapping your app
-
Verify suppressHydrationWarning on <html> tag
-
Ensure dark mode classes are defined in CSS
Form validation issues
-
Install required packages: react-hook-form , @hookform/resolvers , zod
-
Check schema matches form fields
-
Verify resolver is configured correctly
TypeScript errors
-
Update @types/react and @types/react-dom
-
Check component prop types
-
Ensure TypeScript version is compatible (>= 4.5)
Resources
-
Documentation: https://ui.shadcn.com
-
GitHub: https://github.com/shadcn-ui/ui
-
Component Registry: https://ui.shadcn.com/docs/components
-
Examples: https://ui.shadcn.com/examples
-
Figma Design Kit: https://ui.shadcn.com/figma
-
v0 (AI UI Generator): https://v0.dev
Implementation Checklist
When implementing shadcn/ui:
-
Run npx shadcn@latest init to set up project
-
Configure Tailwind CSS and path aliases
-
Set up dark mode with ThemeProvider
-
Install required components via CLI
-
Create utility functions (cn helper)
-
Set up form handling (React Hook Form + Zod)
-
Configure icons (lucide-react)
-
Implement theme toggle component
-
Test components in both light and dark modes
-
Customize color palette if needed
-
Add loading states with Skeleton
-
Implement error handling patterns
-
Test accessibility features
-
Optimize bundle size (tree-shaking)
-
Add responsive design utilities