shadcn-ui

📁 bobmatnyc/claude-mpm-skills 📅 Jan 23, 2026
90
总安装量
90
周安装量
#2553
全站排名
安装命令
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill shadcn-ui

Agent 安装分布

claude-code 75
opencode 69
gemini-cli 62
cursor 58
codex 56
antigravity 53

Skill 文档

shadcn/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: npm install component-library → locked to package versions
  • shadcn/ui: Components copied to components/ui/ → you own the code

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:

  • style: “default” or “new-york” (design variants)
  • rsc: React Server Components support
  • cssVariables: Use CSS variables for theming
  • prefix: Tailwind class prefix (optional)

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:

  • accordion – Collapsible content sections
  • alert – Contextual feedback messages
  • alert-dialog – Interrupting modal dialogs
  • avatar – User profile images
  • badge – Status indicators
  • calendar – Date picker
  • checkbox – Binary input
  • command – Command palette (⌘K menu)
  • context-menu – Right-click menus
  • dropdown-menu – Dropdown menus
  • hover-card – Hover tooltips
  • input – Text input
  • label – Form labels
  • menubar – Application menu bar
  • navigation-menu – Site navigation
  • popover – Floating panels
  • progress – Progress indicators
  • radio-group – Radio button groups
  • scroll-area – Custom scrollbars
  • select – Dropdown selects
  • separator – Visual dividers
  • sheet – Side panels
  • skeleton – Loading placeholders
  • slider – Range input
  • switch – Toggle switch
  • tabs – Tab navigation
  • textarea – Multi-line input
  • toast – Notification toasts
  • toggle – Toggle button
  • tooltip – Hover tooltips

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 / Shift+Tab – Navigate between interactive elements
  • Enter / Space – Activate buttons
  • Escape – Close dialogs, dropdowns, popovers
  • Arrow keys – Navigate menus, select options, radio groups
  • Home / End – Jump to first/last in lists

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 components/ui/ (don’t mix with app components)
  • Create custom compositions in components/ (outside ui/)
  • Use lib/utils.ts for shared utilities

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 components.json manually (use CLI)

Performance:

  • Tree-shaking automatic (only imports what you use)
  • Use asChild to avoid unnecessary wrapper elements
  • 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 asChild when applicable

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