Learn-skills.dev shadcn-ui
Guide for implementing shadcn/ui - a collection of beautifully-designed, accessible UI components built with Radix UI and Tailwind CSS. Use when building user interfaces, adding UI components, or implementing design systems in React-based applications.
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/aia-11-hn-mib/mib-mockinterviewaibot/shadcn-ui" ~/.claude/skills/neversight-learn-skills-dev-shadcn-ui && rm -rf "$T"
data/skills-md/aia-11-hn-mib/mib-mockinterviewaibot/shadcn-ui/SKILL.mdshadcn/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
directorycomponents/ui/ - 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
aliases match your project structurecomponents.json - Ensure components are installed in correct directory
- Check path aliases in
-
Styling not applied
- Verify Tailwind CSS is configured correctly
- Check
imports CSS variablesglobals.css - Ensure
plugin is installedtailwindcss-animate
-
Dark mode not working
- Check ThemeProvider is wrapping your app
- Verify
onsuppressHydrationWarning
tag<html> - Ensure dark mode classes are defined in CSS
-
Form validation issues
- Install required packages:
,react-hook-form
,@hookform/resolverszod - Check schema matches form fields
- Verify resolver is configured correctly
- Install required packages:
-
TypeScript errors
- Update
and@types/react@types/react-dom - Check component prop types
- Ensure TypeScript version is compatible (>= 4.5)
- Update
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
to set up projectnpx shadcn@latest init - 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