git clone https://github.com/MacPhobos/research-mind
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-ui-components-shadcn" ~/.claude/skills/macphobos-research-mind-toolchains-ui-components-shadcn && rm -rf "$T"
.claude/skills/toolchains-ui-components-shadcn/skill.mdshadcn/ui - Component Library
progressive_disclosure: entry_point: summary, when_to_use, quick_start estimated_tokens: entry: 85 full: 4800
Summary
shadcn/ui is a collection of re-usable React components built with Radix UI primitives and styled with Tailwind CSS. Unlike traditional component libraries, shadcn/ui components are copied directly into your project, giving you full ownership and customization control. Components are accessible, customizable, and open source.
Core Philosophy: Copy-paste components, not npm packages. You own the code.
When to Use
Use shadcn/ui when:
- Building React applications with Tailwind CSS
- Need accessible, production-ready UI components
- Want full control over component code and styling
- Prefer composition over configuration
- Building with Next.js, Vite, Remix, or Astro
- Need dark mode support out of the box
- Want TypeScript-first components
Don't use when:
- Not using Tailwind CSS (core styling dependency)
- Need legacy browser support (uses modern CSS features)
- Prefer packaged npm libraries over code ownership
- Building non-React frameworks (Vue, Svelte, Angular)
Quick Start
Installation
# Initialize shadcn/ui in your project npx shadcn-ui@latest init # Follow interactive prompts: # - TypeScript? (yes/no) # - Style: Default/New York # - Base color: Slate/Gray/Zinc/Neutral/Stone # - CSS variables: (yes/no) # - React Server Components: (yes/no) # - components.json location # - Tailwind config location # - CSS file location # - Import alias (@/components)
Add Your First Component
# Add individual components npx shadcn-ui@latest add button npx shadcn-ui@latest add card npx shadcn-ui@latest add dialog # Add multiple components at once npx shadcn-ui@latest add button card dialog form input
Basic Usage
import { Button } from "@/components/ui/button" import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" export default function Example() { return ( <Card> <CardHeader> <CardTitle>Welcome</CardTitle> </CardHeader> <CardContent> <Button>Click me</Button> </CardContent> </Card> ) }
Architecture
Copy-Paste Philosophy
Key Difference from Traditional Libraries:
- Traditional:
→ locked to package versionsnpm install component-library - shadcn/ui: Components copied to
→ you own the codecomponents/ui/
Benefits:
- Full customization control
- No breaking changes from package updates
- Easy to modify for specific needs
- Transparent implementation
- Tree-shakeable by default
Component Structure
src/ ├── components/ │ └── ui/ │ ├── button.tsx # Component implementation │ ├── card.tsx # Owns its code │ ├── dialog.tsx # Modifiable │ └── ... ├── lib/ │ └── utils.ts # cn() helper for class merging └── app/ └── globals.css # Tailwind directives + CSS variables
Technology Stack
Core Dependencies:
- Radix UI: Accessible component primitives (headless UI)
- Tailwind CSS: Utility-first styling
- TypeScript: Type safety
- class-variance-authority (CVA): Variant management
- clsx: Class name concatenation
- tailwind-merge: Conflict-free class merging
Radix UI Integration:
// shadcn/ui components wrap Radix primitives import * as DialogPrimitive from "@radix-ui/react-dialog" // Add styling and variants const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger const DialogContent = React.forwardRef<...>( ({ className, children, ...props }, ref) => ( <DialogPrimitive.Content ref={ref} className={cn("fixed ...", className)} {...props} /> ) )
Configuration
components.json
{ "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" } }
Key Options:
: "default" or "new-york" (design variants)style
: React Server Components supportrsc
: Use CSS variables for themingcssVariables
: Tailwind class prefix (optional)prefix
Tailwind Configuration
// tailwind.config.ts import type { Config } from "tailwindcss" const config = { darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, 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))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config export default config
CSS Variables (globals.css)
@tailwind base; @tailwind components; @tailwind utilities; @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%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } }
Component Catalog
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"><Icon /></Button> // States <Button disabled>Disabled</Button> <Button asChild> <Link href="/about">As Link</Link> </Button>
Implementation Pattern (CVA):
import { cva, type VariantProps } from "class-variance-authority" const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 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", }, } )
Card
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, } 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>
Dialog (Modal)
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, } 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> <DialogFooter> <Button variant="outline">Cancel</Button> <Button>Confirm</Button> </DialogFooter> </DialogContent> </Dialog>
Form (with react-hook-form + zod)
import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" 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, { message: "Username must be at least 2 characters.", }), email: z.string().email({ message: "Please enter a valid email address.", }), }) 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> )} /> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input type="email" placeholder="user@example.com" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> ) }
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>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> <TableCell>INV001</TableCell> <TableCell>Paid</TableCell> <TableCell>Credit Card</TableCell> <TableCell className="text-right">$250.00</TableCell> </TableRow> </TableBody> </Table>
Additional Components
Available via CLI:
- Collapsible content sectionsaccordion
- Contextual feedback messagesalert
- Interrupting modal dialogsalert-dialog
- User profile imagesavatar
- Status indicatorsbadge
- Date pickercalendar
- Binary inputcheckbox
- Command palette (⌘K menu)command
- Right-click menuscontext-menu
- Dropdown menusdropdown-menu
- Hover tooltipshover-card
- Text inputinput
- Form labelslabel
- Application menu barmenubar
- Site navigationnavigation-menu
- Floating panelspopover
- Progress indicatorsprogress
- Radio button groupsradio-group
- Custom scrollbarsscroll-area
- Dropdown selectsselect
- Visual dividersseparator
- Side panelssheet
- Loading placeholdersskeleton
- Range inputslider
- Toggle switchswitch
- Tab navigationtabs
- Multi-line inputtextarea
- Notification toaststoast
- Toggle buttontoggle
- Hover tooltipstooltip
Theming
Color Customization
Change base color scheme:
# Regenerate components with new base color npx shadcn-ui@latest init # Choose new base: Slate, Gray, Zinc, Neutral, Stone
Manual color override (globals.css):
:root { --primary: 210 100% 50%; /* HSL: Blue */ --primary-foreground: 0 0% 100%; } .dark { --primary: 210 100% 60%; /* Lighter blue for dark mode */ }
Custom Variants
// Extend button variants const buttonVariants = cva( "...", { variants: { variant: { // ...existing variants gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white", }, }, } ) // Usage <Button variant="gradient">Gradient Button</Button>
Theme Switching
// Using next-themes import { ThemeProvider } from "next-themes" // app/layout.tsx export default function RootLayout({ children }) { return ( <html lang="en" suppressHydrationWarning> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> ) } // Theme toggle component 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> ) }
Dark Mode
Setup with Next.js
npm install next-themes
// app/providers.tsx "use client" import { ThemeProvider } from "next-themes" export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> ) } // app/layout.tsx import { Providers } from "./providers" export default function RootLayout({ children }) { return ( <html lang="en" suppressHydrationWarning> <body> <Providers>{children}</Providers> </body> </html> ) }
Dark Mode Utilities
// Force dark mode for specific section <div className="dark"> <Card>Always dark, regardless of theme</Card> </div> // Conditional styling <div className="bg-white dark:bg-slate-950"> <p className="text-slate-900 dark:text-slate-50"> Adapts to theme </p> </div>
Next.js Integration
App Router Setup
# Create Next.js app with TypeScript and Tailwind npx create-next-app@latest my-app --typescript --tailwind --app # Initialize shadcn/ui cd my-app npx shadcn-ui@latest init # Add components npx shadcn-ui@latest add button card form
Server Components
// app/page.tsx (Server Component by default) import { Button } from "@/components/ui/button" export default function HomePage() { return ( <main> <h1>Welcome</h1> {/* Static components work in Server Components */} <Button asChild> <a href="/about">Learn More</a> </Button> </main> ) }
Client Components
// app/interactive.tsx "use client" import { useState } from "react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog" export function InteractiveSection() { const [open, setOpen] = useState(false) return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button>Open Dialog</Button> </DialogTrigger> <DialogContent> <p>Client-side interactivity</p> </DialogContent> </Dialog> ) }
Route Handlers
// app/api/submit/route.ts import { NextResponse } from "next/server" import { z } from "zod" const formSchema = z.object({ email: z.string().email(), message: z.string().min(10), }) export async function POST(request: Request) { try { const body = await request.json() const validatedData = formSchema.parse(body) // Process form data return NextResponse.json({ success: true }) } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json({ errors: error.errors }, { status: 400 }) } return NextResponse.json({ error: "Internal error" }, { status: 500 }) } }
Accessibility
ARIA Support
All shadcn/ui components include proper ARIA attributes via Radix UI:
// Dialog automatically includes: // - role="dialog" // - aria-describedby // - aria-labelledby // - Focus trap // - Escape key handler <Dialog> <DialogContent> {/* Automatically accessible */} </DialogContent> </Dialog> // Button includes: // - role="button" // - tabindex="0" // - Keyboard activation (Space/Enter) <Button>Accessible by default</Button>
Keyboard Navigation
Built-in keyboard support:
/Tab
- Navigate between interactive elementsShift+Tab
/Enter
- Activate buttonsSpace
- Close dialogs, dropdowns, popoversEscape
- Navigate menus, select options, radio groupsArrow keys
/Home
- Jump to first/last in listsEnd
Example: Command Palette:
import { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from "@/components/ui/command" // ⌘K to open <CommandDialog open={open} onOpenChange={setOpen}> <CommandInput placeholder="Type a command..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search Emoji</CommandItem> <CommandItem>Calculator</CommandItem> </CommandGroup> </CommandList> </CommandDialog>
Screen Reader Support
// Visually hidden but accessible to screen readers <span className="sr-only">Close dialog</span> // Skip navigation links <a href="#main-content" className="sr-only focus:not-sr-only"> Skip to main content </a> // Descriptive labels <FormLabel htmlFor="email">Email address</FormLabel> <Input id="email" type="email" aria-describedby="email-description" aria-invalid={!!errors.email} /> <FormDescription id="email-description"> We'll never share your email. </FormDescription>
Focus Management
// Focus trap in Dialog (automatic) <Dialog> <DialogContent> {/* Focus stays within dialog until closed */} </DialogContent> </Dialog> // Custom focus management import { useRef, useEffect } from "react" function CustomComponent() { const inputRef = useRef<HTMLInputElement>(null) useEffect(() => { inputRef.current?.focus() }, []) return <Input ref={inputRef} /> }
Composition Patterns
Compound Components
// Card composition <Card> <CardHeader> <CardTitle>Title</CardTitle> <CardDescription>Description</CardDescription> </CardHeader> <CardContent>Content</CardContent> <CardFooter>Footer</CardFooter> </Card> // Form composition <Form {...form}> <FormField control={form.control} name="field" render={({ field }) => ( <FormItem> <FormLabel>Label</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormDescription>Help text</FormDescription> <FormMessage /> </FormItem> )} /> </Form>
Polymorphic Components (asChild)
// Render Button as Link import { Button } from "@/components/ui/button" import Link from "next/link" <Button asChild> <Link href="/dashboard">Go to Dashboard</Link> </Button> // Render as custom component <Button asChild> <motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > Animated Button </motion.button> </Button>
How it works (Radix Slot):
import { Slot } from "@radix-ui/react-slot" interface ButtonProps { asChild?: boolean } const Button = ({ asChild, ...props }: ButtonProps) => { const Comp = asChild ? Slot : "button" return <Comp {...props} /> }
Custom Compositions
// Create custom card variant export function PricingCard({ title, price, features, highlighted }: PricingCardProps) { return ( <Card className={cn(highlighted && "border-primary")}> <CardHeader> <CardTitle>{title}</CardTitle> <CardDescription className="text-3xl font-bold"> ${price}/mo </CardDescription> </CardHeader> <CardContent> <ul className="space-y-2"> {features.map((feature) => ( <li key={feature} className="flex items-center"> <Check className="mr-2 h-4 w-4 text-primary" /> {feature} </li> ))} </ul> </CardContent> <CardFooter> <Button className="w-full" variant={highlighted ? "default" : "outline"}> Get Started </Button> </CardFooter> </Card> ) }
CLI Commands
Initialize
# Interactive init npx shadcn-ui@latest init # Non-interactive with defaults npx shadcn-ui@latest init -y # Specify options npx shadcn-ui@latest init --typescript --tailwind
Add Components
# Single component npx shadcn-ui@latest add button # Multiple components npx shadcn-ui@latest add button card dialog form # All components (not recommended - adds everything) npx shadcn-ui@latest add --all # Specific version npx shadcn-ui@latest add button@1.0.0 # Overwrite existing npx shadcn-ui@latest add button --overwrite # Different path npx shadcn-ui@latest add button --path src/components/ui
Diff Components
# Check for component updates npx shadcn-ui@latest diff # Diff specific component npx shadcn-ui@latest diff button # Show what would change npx shadcn-ui@latest diff --check
Update Components
# Update all components npx shadcn-ui@latest update # Update specific components npx shadcn-ui@latest update button card # Preview changes before applying npx shadcn-ui@latest update --dry-run
Advanced Patterns
Custom Hooks
// useToast hook (built-in with toast component) import { useToast } from "@/components/ui/use-toast" function MyComponent() { const { toast } = useToast() return ( <Button onClick={() => { toast({ title: "Scheduled: Catch up", description: "Friday, February 10, 2023 at 5:57 PM", }) }} > Show Toast </Button> ) } // Custom form hook import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" function useFormWithToast<T extends z.ZodType>(schema: T) { const { toast } = useToast() const form = useForm({ resolver: zodResolver(schema), }) const handleSubmit = form.handleSubmit(async (data) => { try { // Submit logic toast({ title: "Success!" }) } catch (error) { toast({ title: "Error", variant: "destructive" }) } }) return { form, handleSubmit } }
Responsive Design
// Mobile-first responsive components <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <Card>Mobile: 1 col, Tablet: 2 col, Desktop: 3 col</Card> </div> // Responsive dialog (sheet on mobile, dialog on desktop) import { useMediaQuery } from "@/hooks/use-media-query" import { Dialog, DialogContent } from "@/components/ui/dialog" import { Sheet, SheetContent } from "@/components/ui/sheet" function ResponsiveModal({ children, ...props }) { const isDesktop = useMediaQuery("(min-width: 768px)") if (isDesktop) { return ( <Dialog {...props}> <DialogContent>{children}</DialogContent> </Dialog> ) } return ( <Sheet {...props}> <SheetContent>{children}</SheetContent> </Sheet> ) }
Animation Variants
// Using Framer Motion with shadcn/ui import { motion } from "framer-motion" import { Card } from "@/components/ui/card" const MotionCard = motion(Card) <MotionCard initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} > Animated Card </MotionCard> // Staggered list animation const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.1 } } } const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } } <motion.ul variants={container} initial="hidden" animate="show"> {items.map((item) => ( <motion.li key={item.id} variants={item}> <Card>{item.content}</Card> </motion.li> ))} </motion.ul>
Best Practices
Code Organization:
- Keep shadcn/ui components in
(don't mix with app components)components/ui/ - Create custom compositions in
(outside ui/)components/ - Use
for shared utilitieslib/utils.ts
Customization:
- Modify components directly in your project (you own the code)
- Use CSS variables for theme-wide changes
- Extend variants with CVA for new styles
- Don't edit
manually (use CLI)components.json
Performance:
- Tree-shaking automatic (only imports what you use)
- Use
to avoid unnecessary wrapper elementsasChild - Lazy load heavy components (Calendar, Command)
- Prefer Server Components when possible (Next.js)
Accessibility:
- Don't remove ARIA attributes from components
- Test keyboard navigation for custom compositions
- Maintain focus management in dialogs/modals
- Use semantic HTML with
when applicableasChild
TypeScript:
- Leverage exported types (ButtonProps, CardProps, etc.)
- Use VariantProps for variant type safety
- Add strict null checks for form validation
Troubleshooting
Import errors:
# Check path aliases in tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
Tailwind classes not applying:
// Ensure content paths include your components // tailwind.config.ts content: [ './src/components/**/*.{ts,tsx}', // Add this './src/app/**/*.{ts,tsx}', ]
Dark mode not working:
// Add suppressHydrationWarning to <html> <html lang="en" suppressHydrationWarning>
Form validation not triggering:
// Ensure FormMessage is included in FormField <FormField> <FormItem> <FormControl>...</FormControl> <FormMessage /> {/* Required for errors */} </FormItem> </FormField>
Resources
- Official Docs: https://ui.shadcn.com
- Component Source: https://github.com/shadcn-ui/ui
- Radix UI Docs: https://www.radix-ui.com
- Tailwind CSS: https://tailwindcss.com
- CVA Docs: https://cva.style/docs
- Examples: https://ui.shadcn.com/examples
- Community: https://discord.gg/shadcn-ui