Back to Skills
    🦞

    shadcn-ui

    Use when building UI with shadcn/ui components, Tailwind CSS

    By @jgarrison929
    View on GitHub
    SKILL.md
    ---
    name: shadcn-ui
    version: 1.0.0
    description: Use when building UI with shadcn/ui components, Tailwind CSS layouts, form patterns with react-hook-form and zod, theming, dark mode, sidebar layouts, mobile navigation, or any shadcn component question.
    triggers:
      - shadcn
      - shadcn/ui
      - radix
      - component library
      - UI components
      - form pattern
      - react-hook-form
      - dark mode
      - theming
      - sidebar layout
      - dialog
      - sheet
      - toast
      - dropdown menu
      - command palette
      - data table
    role: specialist
    scope: implementation
    output-format: code
    ---
    
    # shadcn/ui Expert
    
    Comprehensive guide for building production UIs with shadcn/ui, Tailwind CSS, react-hook-form, and zod.
    
    ## Core Concepts
    
    shadcn/ui is **not** a component library — it's a collection of copy-paste components built on Radix UI primitives. You own the code. Components are added to your project, not installed as dependencies.
    
    ## Installation
    
    ```bash
    # Initialize shadcn/ui in a Next.js project
    npx shadcn@latest init
    
    # Add individual components
    npx shadcn@latest add button
    npx shadcn@latest add card
    npx shadcn@latest add dialog
    npx shadcn@latest add form
    npx shadcn@latest add input
    npx shadcn@latest add select
    npx shadcn@latest add table
    npx shadcn@latest add toast
    npx shadcn@latest add dropdown-menu
    npx shadcn@latest add sheet
    npx shadcn@latest add tabs
    npx shadcn@latest add sidebar
    
    # Add multiple at once
    npx shadcn@latest add button card input label textarea select checkbox
    ```
    
    ---
    
    ## Component Categories & When to Use
    
    ### Layout & Navigation
    | Component | Use When |
    |-----------|----------|
    | `sidebar` | App-level navigation with collapsible sections |
    | `navigation-menu` | Top-level site navigation with dropdowns |
    | `breadcrumb` | Showing page hierarchy/location |
    | `tabs` | Switching between related views in same context |
    | `separator` | Visual divider between content sections |
    | `sheet` | Slide-out panel (mobile nav, filters, detail views) |
    | `resizable` | Adjustable panel layouts |
    
    ### Forms & Input
    | Component | Use When |
    |-----------|----------|
    | `form` | Any form with validation (wraps react-hook-form) |
    | `input` | Text, email, password, number inputs |
    | `textarea` | Multi-line text input |
    | `select` | Choosing from a list (native-like) |
    | `combobox` | Searchable select (uses `command` + `popover`) |
    | `checkbox` | Boolean or multi-select toggles |
    | `radio-group` | Single selection from small set |
    | `switch` | On/off toggle (settings, preferences) |
    | `slider` | Numeric range selection |
    | `date-picker` | Date selection (uses `calendar` + `popover`) |
    | `toggle` | Pressed/unpressed state (toolbar buttons) |
    
    ### Feedback & Overlay
    | Component | Use When |
    |-----------|----------|
    | `dialog` | Modal confirmation, forms, or detail views |
    | `alert-dialog` | Destructive action confirmation ("Are you sure?") |
    | `sheet` | Side panel for forms, filters, mobile nav |
    | `toast` | Brief non-blocking notifications (via `sonner`) |
    | `alert` | Inline status messages (info, warning, error) |
    | `tooltip` | Hover hints for icons/buttons |
    | `popover` | Rich content on click (color pickers, date pickers) |
    | `hover-card` | Preview content on hover (user profiles, links) |
    | `skeleton` | Loading placeholders |
    | `progress` | Task completion indicators |
    
    ### Data Display
    | Component | Use When |
    |-----------|----------|
    | `table` | Tabular data display |
    | `data-table` | Tables with sorting, filtering, pagination (uses `@tanstack/react-table`) |
    | `card` | Content containers with header, body, footer |
    | `badge` | Status labels, tags, counts |
    | `avatar` | User profile images |
    | `accordion` | Collapsible FAQ or settings sections |
    | `carousel` | Image/content slideshows |
    | `scroll-area` | Custom scrollable containers |
    
    ### Actions
    | Component | Use When |
    |-----------|----------|
    | `button` | Primary actions, form submissions |
    | `dropdown-menu` | Context menus, action menus |
    | `context-menu` | Right-click menus |
    | `menubar` | Application menu bars |
    | `command` | Command palette / search (⌘K) |
    
    ---
    
    ## Form Patterns (react-hook-form + zod)
    
    ### Complete Form Example
    
    ```bash
    npx shadcn@latest add form input select textarea checkbox button
    ```
    
    ```tsx
    'use client'
    
    import { zodResolver } from '@hookform/resolvers/zod'
    import { useForm } from 'react-hook-form'
    import { 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'
    import { Textarea } from '@/components/ui/textarea'
    import {
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from '@/components/ui/select'
    import { Checkbox } from '@/components/ui/checkbox'
    import { toast } from 'sonner'
    
    const formSchema = z.object({
      name: z.string().min(2, 'Name must be at least 2 characters'),
      email: z.string().email('Invalid email address'),
      role: z.enum(['admin', 'user', 'editor'], { required_error: 'Select a role' }),
      bio: z.string().max(500).optional(),
      notifications: z.boolean().default(false),
    })
    
    type FormValues = z.infer<typeof formSchema>
    
    export function UserForm() {
      const form = useForm<FormValues>({
        resolver: zodResolver(formSchema),
        defaultValues: {
          name: '',
          email: '',
          bio: '',
          notifications: false,
        },
      })
    
      async function onSubmit(values: FormValues) {
        try {
          await createUser(values)
          toast.success('User created successfully')
          form.reset()
        } catch (error) {
          toast.error('Failed to create user')
        }
      }
    
      return (
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField
              control={form.control}
              name="name"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Name</FormLabel>
                  <FormControl>
                    <Input placeholder="John Doe" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
    
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input type="email" placeholder="john@example.com" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
    
            <FormField
              control={form.control}
              name="role"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Role</FormLabel>
                  <Select onValueChange={field.onChange} defaultValue={field.value}>
                    <FormControl>
                      <SelectTrigger>
                        <SelectValue placeholder="Select a role" />
                      </SelectTrigger>
                    </FormControl>
                    <SelectContent>
                      <SelectItem value="admin">Admin</SelectItem>
                      <SelectItem value="editor">Editor</SelectItem>
                      <SelectItem value="user">User</SelectItem>
                    </SelectContent>
                  </Select>
                  <FormMessage />
                </FormItem>
              )}
            />
    
            <FormField
              control={form.control}
              name="bio"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Bio</FormLabel>
                  <FormControl>
                    <Textarea placeholder="Tell us about yourself..." {...field} />
                  </FormControl>
                  <FormDescription>Max 500 characters</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
    
            <FormField
              control={form.control}
              name="notifications"
              render={({ field }) => (
                <FormItem className="flex flex-row items-start space-x-3 space-y-0">
                  <FormControl>
                    <Checkbox checked={field.value} onCheckedChange={field.onChange} />
                  </FormControl>
                  <div className="space-y-1 leading-none">
                    <FormLabel>Email notifications</FormLabel>
                    <FormDescription>Receive emails about account activity</FormDescription>
                  </div>
                </FormItem>
              )}
            />
    
            <Button type="submit" disabled={form.formState.isSubmitting}>
              {form.formState.isSubmitting ? 'Creating...' : 'Create User'}
            </Button>
          </form>
        </Form>
      )
    }
    ```
    
    ### Form with Server Action
    
    ```tsx
    'use client'
    
    import { useFormState } from 'react-dom'
    import { useForm } from 'react-hook-form'
    import { zodResolver } from '@hookform/resolvers/zod'
    
    export function ContactForm() {
      const form = useForm<FormValues>({
        resolver: zodResolver(schema),
      })
    
      async function onSubmit(values: FormValues) {
        const formData = new FormData()
        Object.entries(values).forEach(([key, value]) => formData.append(key, String(value)))
        await submitContact(formData)
      }
    
      return (
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            {/* fields */}
          </form>
        </Form>
      )
    }
    ```
    
    ---
    
    ## Theming & Dark Mode
    
    ### Setup with next-themes
    
    ```bash
    npm install next-themes
    npx shadcn@latest add dropdown-menu
    ```
    
    ```tsx
    // app/providers.tsx
    'use client'
    import { ThemeProvider } from 'next-themes'
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
        </ThemeProvider>
      )
    }
    ```
    
    ```tsx
    // components/theme-toggle.tsx
    'use client'
    import { Moon, Sun } from 'lucide-react'
    import { useTheme } from 'next-themes'
    import { Button } from '@/components/ui/button'
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuTrigger,
    } from '@/component
    
    ... (truncated)