shadcn-ui

SKILL.md

shadcn/ui - Component Library Guide

shadcn/ui is a collection of re-usable components built using Radix UI and Tailwind CSS. You can copy and paste the code into your project or use the CLI to add components automatically.

Key Concepts

It's NOT a traditional component library - you own the code. Components are copied into your project, giving you full control to modify them as needed.

Built on:

  • Radix UI (accessible primitives)
  • Tailwind CSS (styling)
  • TypeScript (type safety)

Supported frameworks: Next.js, Vite, Remix, Astro, Laravel, Gatsby, React Router, TanStack Router/Start


Installation

Detect the project type first

Check which framework and package manager the project uses:

# Check framework
ls next.config.*  # Next.js
ls vite.config.*  # Vite
ls remix.config.* # Remix

# Check package manager
ls package-lock.json # npm
ls yarn.lock      # yarn
ls pnpm-lock.yaml # pnpm

Step 1: Install dependencies

For Next.js:

npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react

For Vite/React:

npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react

Step 2: Configure Tailwind

Create or update tailwind.config.js:

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)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
} satisfies Config

export default config

Step 3: Create utilities file

Create src/lib/utils.ts (or lib/utils.ts):

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Step 4: Add CSS variables

Create or update app/globals.css (Next.js App Router) or src/index.css (Vite):

@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;
  }
}

Using the CLI

Initialize shadcn/ui

npx shadcn@latest init

This command:

  • Creates components.json configuration
  • Sets up the project structure
  • Configures paths and aliases

Add components

# Add a single component
npx shadcn@latest add button

# Add multiple components
npx shadcn@latest add button card input label

# Add with --yes flag to skip prompts
npx shadcn@latest add button --yes

# Add to specific path
npx shadcn@latest add button --path src/components

Common components to add first:

  • button - Essential for any UI
  • card - Container for content
  • input - Form inputs
  • label - Form labels
  • form - Form handling with React Hook Form

Popular Components

Button

import { Button } from "@/components/ui/button"

export default function Page() {
  return (
    <div>
      <Button>Click me</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="outline">Cancel</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
      <Button size="sm">Small</Button>
      <Button size="lg">Large</Button>
    </div>
  )
}

Card

import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"

export default function Page() {
  return (
    <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>
  )
}

Form (with React Hook Form)

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  username: z.string().min(2).max(50),
  email: z.string().email(),
})

export default 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>
  )
}

Dialog (Modal)

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export default function DialogDemo() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">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>
  )
}

Table

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

export default function TableDemo() {
  return (
    <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>
  )
}

Dark Mode Setup

Using next-themes (Recommended for Next.js)

npm install next-themes

Create 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>
}

Update app/layout.tsx:

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Add theme toggle:

npx shadcn@latest add theme-toggle
import { ThemeToggle } from "@/components/ui/theme-toggle"

export default function Navbar() {
  return (
    <nav>
      <ThemeToggle />
    </nav>
  )
}

Customization

Modify colors

Edit the CSS variables in your globals.css:

:root {
  --primary: 220 90% 56%; /* Custom blue */
  --secondary: 160 60% 45%; /* Custom green */
  /* ... */
}

Customize component styles

Each component is in components/ui/. You can modify them directly:

// components/ui/button.tsx
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 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none",
  {
    variants: {
      // Add your custom variant
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        mycustom: "bg-purple-500 text-white hover:bg-purple-600",
        // ...
      },
    },
  }
)

Component Categories

Form & Input

  • Button, Input, Textarea, Checkbox, Radio Group, Select, Switch, Slider, Calendar, Combobox, Label, Field, Form

Layout & Navigation

  • Accordion, Breadcrumb, Navigation Menu, Sidebar, Tabs, Separator, Scroll Area, Resizable

Overlays & Dialogs

  • Dialog, Alert Dialog, Sheet, Drawer, Popover, Tooltip, Hover Card, Context Menu, Dropdown Menu, Menubar, Command

Feedback & Status

  • Alert, Toast, Progress, Spinner, Skeleton, Badge, Empty

Display & Media

  • Avatar, Card, Table, Data Table, Chart, Carousel, Typography, Item, Kbd

Common Workflows

Creating a form

  1. Add form, input, label, button components
  2. Install React Hook Form and Zod
  3. Create schema with Zod
  4. Build form with Form components
  5. Handle submission

Building a dashboard

  1. Add card, button, table components
  2. Create layout with sidebar or tabs
  3. Add data visualization with chart
  4. Include progress for loading states

Creating a modal dialog

  1. Add dialog component
  2. Use DialogTrigger for button
  3. Add DialogContent with title/description
  4. Handle confirmation in DialogFooter

Adding dark mode

  1. Install next-themes
  2. Create ThemeProvider wrapper
  3. Add theme-toggle component
  4. Test both light/dark modes

Best Practices

  1. Always add components via CLI - npx shadcn@latest add [component]
  2. Customize after adding - Components are yours to modify
  3. Use the cn() utility - Merge Tailwind classes properly
  4. Check dependencies - Some components need extra packages (React Hook Form, Zod, etc.)
  5. Read component docs - Visit https://ui.shadcn.com/docs/components/[name] for examples

Troubleshooting

Styles not working:

  • Check Tailwind CSS is configured correctly
  • Verify content paths in tailwind.config.js
  • Ensure CSS variables are defined

Components not found:

  • Make sure you ran npx shadcn@latest add [component]
  • Check the component path in your import

TypeScript errors:

  • Ensure all dependencies are installed
  • Check tsconfig.json path aliases (@/components)

Dark mode not working:

  • Verify ThemeProvider wraps your app
  • Check CSS variables have .dark class
  • Ensure suppressHydrationWarning on html tag
Weekly Installs
1
First Seen
10 days ago
Installed on
claude-code1