shadcn-ui

shadcn/ui component library for React with Tailwind CSS - copy-paste accessible components with full code ownership

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill shadcn-ui
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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 &#x26;&#x26; "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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card