--- name: shadcn-ui description: Provides complete shadcn/ui component library patterns including installation, configuration, and implementation of accessible React components. Use when setting up shadcn/ui, installing components, building forms with React Hook Form and Zod, customizing themes with Tailwind CSS, or implementing UI patterns like buttons, dialogs, dropdowns, tables, and complex form layouts. allowed-tools: Read, Write, Bash, Edit, Glob --- # shadcn/ui Component Patterns ## Overview Expert guide for building accessible, customizable UI components with shadcn/ui, Radix UI, and Tailwind CSS. This skill provides comprehensive patterns for implementing production-ready components with full accessibility support. > **Mosaic Stack Note:** This skill's Tailwind configuration examples reference v3 patterns (`tailwind.config.js`, `@tailwind` directives, `require()` plugins). Mosaic Stack uses **Tailwind CSS v4** which uses CSS-native `@import "tailwindcss"` and `@theme` blocks instead. Component usage patterns and Radix UI primitives remain accurate regardless of Tailwind version. For Tailwind v4 patterns, also load the `tailwind-design-system` skill. ## Table of Contents - [When to Use](#when-to-use) - [Quick Start](#quick-start) - [Installation & Setup](#installation--setup) - [Project Configuration](#project-configuration) - [Core Components](#core-components) - [Button](#button-component) - [Input & Form Fields](#input--form-fields) - [Forms with Validation](#forms-with-validation) - [Card](#card-component) - [Dialog (Modal)](#dialog-modal-component) - [Select (Dropdown)](#select-dropdown-component) - [Sheet (Slide-over)](#sheet-slide-over-component) - [Menubar & Navigation](#menubar--navigation) - [Table](#table-component) - [Toast Notifications](#toast-notifications) - [Charts](#charts-component) - [Advanced Patterns](#advanced-patterns) - [Customization](#customization) - [Next.js Integration](#nextjs-integration) - [Best Practices](#best-practices) - [Common Component Combinations](#common-component-combinations) ## When to Use - Setting up a new project with shadcn/ui - Installing or configuring individual components - Building forms with React Hook Form and Zod validation - Creating accessible UI components (buttons, dialogs, dropdowns, sheets) - Customizing component styling with Tailwind CSS - Implementing design systems with shadcn/ui - Building Next.js applications with TypeScript - Creating complex layouts and data displays ## Instructions 1. **Initialize Project**: Run `npx shadcn@latest init` to configure shadcn/ui 2. **Install Components**: Add components with `npx shadcn@latest add ` 3. **Configure Theme**: Customize CSS variables in globals.css for theming 4. **Import Components**: Use components from `@/components/ui/` directory 5. **Customize as Needed**: Modify component code directly in your project 6. **Add Form Validation**: Integrate React Hook Form with Zod schemas 7. **Test Accessibility**: Verify ARIA attributes and keyboard navigation ## Examples ### Complete Form with Validation ```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, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; const formSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Password must be at least 8 characters'), }); export function LoginForm() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: '', password: '' }, }); return (
( Email )} /> ); } ``` ## Constraints and Warnings - **Not an NPM Package**: Components are copied to your project; you own the code - **Client Components**: Most components require "use client" directive - **Radix Dependencies**: Ensure all @radix-ui packages are installed - **Tailwind Required**: Components rely on Tailwind CSS utilities - **TypeScript**: Designed for TypeScript projects; type definitions included - **Path Aliases**: Configure @ alias in tsconfig.json for imports - **Dark Mode**: Set up dark mode with CSS variables or class strategy ## Quick Start For new projects, use the automated setup: ```bash # Create Next.js project with shadcn/ui npx create-next-app@latest my-app --typescript --tailwind --eslint --app cd my-app npx shadcn@latest init # Install essential components npx shadcn@latest add button input form card dialog select ``` For existing projects: ```bash # Install dependencies npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react # Initialize shadcn/ui npx shadcn@latest init ``` ## What is shadcn/ui? shadcn/ui is **not** a traditional component library or npm package. Instead: - It's a **collection of reusable components** that you can copy into your project - Components are **yours to customize** - you own the code - Built with **Radix UI** primitives for accessibility - Styled with **Tailwind CSS** utilities - Includes CLI tool for easy component installation ## Installation & Setup ### Initial Setup ```bash # Initialize shadcn/ui in your project npx shadcn@latest init ``` During setup, you'll configure: - TypeScript or JavaScript - Style (Default, New York, etc.) - Base color theme - CSS variables or Tailwind CSS classes - Component installation path ### Installing Individual Components ```bash # Install a single component npx shadcn@latest add button # Install multiple components npx shadcn@latest add button input form # Install all components npx shadcn@latest add --all ``` ### Manual Installation If you prefer manual setup: ```bash # Install dependencies for a specific component npm install @radix-ui/react-slot # Copy component code from ui.shadcn.com # Place in src/components/ui/ ``` ## Project Configuration ### Required Dependencies ```json { "dependencies": { "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "lucide-react": "^0.294.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7" } } ``` ### TSConfig Configuration ```json { "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "es6"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "baseUrl": ".", "paths": { "@/components/*": ["./src/components/*"], "@/lib/*": ["./src/lib/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` ### Tailwind Configuration ```js // tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { 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')], }; ``` ### CSS Variables (globals.css) ```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; } } ``` ## Core Components ### Button Component Installation: ```bash npx shadcn@latest add button ``` Basic usage: ```tsx import { Button } from '@/components/ui/button'; export function ButtonDemo() { return ; } ``` Button variants: ```tsx import { Button } from '@/components/ui/button'; export function ButtonVariants() { return (
); } ``` Button sizes: ```tsx
``` With loading state: ```tsx import { Button } from '@/components/ui/button'; import { Loader2 } from 'lucide-react'; export function ButtonLoading() { return ( ); } ``` ### Input & Form Fields #### Input Component Installation: ```bash npx shadcn@latest add input ``` Basic input: ```tsx import { Input } from '@/components/ui/input'; export function InputDemo() { return ; } ``` Input with label: ```tsx import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; export function InputWithLabel() { return (
); } ``` Input with button: ```tsx import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; export function InputWithButton() { return (
); } ``` ### Forms with Validation Installation: ```bash npx shadcn@latest add form ``` This installs React Hook Form, Zod, and form components. Complete form example: ```tsx 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; 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'; import { toast } from '@/components/ui/use-toast'; 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.', }), }); export function ProfileForm() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { username: '', email: '', }, }); function onSubmit(values: z.infer) { toast({ title: 'You submitted the following values:', description: (
          {JSON.stringify(values, null, 2)}
        
), }); } return (
( Username This is your public display name. )} /> ( Email )} /> ); } ``` ### Card Component Installation: ```bash npx shadcn@latest add card ``` Basic card: ```tsx import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; export function CardDemo() { return ( Card Title Card Description

Card Content

Card Footer

); } ``` Card with form: ```tsx import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; export function CardWithForm() { return ( Create project Deploy your new project in one-click.
); } ``` ### Dialog (Modal) Component Installation: ```bash npx shadcn@latest add dialog ``` Basic dialog: ```tsx import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; export function DialogDemo() { return ( Edit profile Make changes to your profile here. Click save when you're done.
); } ``` ### Sheet (Slide-over) Component Installation: ```bash npx shadcn@latest add sheet ``` Basic sheet: ```tsx import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'; export function SheetDemo() { return ( Edit profile Make changes to your profile here. Click save when you're done.
); } ``` Sheet with side placement: ```tsx Settings Configure your application settings here. {/* Settings content */} ``` ### Menubar & Navigation #### Menubar Component Installation: ```bash npx shadcn@latest add menubar ``` Basic menubar: ```tsx import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, } from '@/components/ui/menubar'; export function MenubarDemo() { return ( File New Tab ⌘T New Window ⌘N Share Print Edit Undo ⌘Z Redo ⌘Y Find Search the web Find... Find Next Find Previous ); } ``` ### Select (Dropdown) Component Installation: ```bash npx shadcn@latest add select ``` Basic select: ```tsx import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; export function SelectDemo() { return ( ); } ``` Select in form: ```tsx ( Role )} /> ``` ### Toast Notifications Installation: ```bash npx shadcn@latest add toast ``` Setup toast provider in root layout: ```tsx import { Toaster } from '@/components/ui/toaster'; export default function RootLayout({ children }) { return ( {children} ); } ``` Using toast: ```tsx import { useToast } from '@/components/ui/use-toast'; import { Button } from '@/components/ui/button'; export function ToastDemo() { const { toast } = useToast(); return ( ); } ``` Toast variants: ```tsx // Success toast({ title: 'Success', description: 'Your changes have been saved.', }); // Error toast({ variant: 'destructive', title: 'Error', description: 'Something went wrong.', }); // With action toast({ title: 'Uh oh! Something went wrong.', description: 'There was a problem with your request.', action: Try again, }); ``` ### Table Component Installation: ```bash npx shadcn@latest add table ``` Basic table: ```tsx import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; const invoices = [ { invoice: 'INV001', status: 'Paid', method: 'Credit Card', amount: '$250.00' }, { invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: '$150.00' }, ]; export function TableDemo() { return ( A list of your recent invoices. Invoice Status Method Amount {invoices.map((invoice) => ( {invoice.invoice} {invoice.status} {invoice.method} {invoice.amount} ))}
); } ``` ### Charts Component Installation: ```bash npx shadcn@latest add chart ``` The charts component in shadcn/ui is built on **Recharts** - providing direct access to all Recharts capabilities with consistent theming and styling. #### ChartContainer and ChartConfig The `ChartContainer` wraps your Recharts component and accepts a `config` prop for theming: ```tsx import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'; import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart'; const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)', }, mobile: { label: 'Mobile', color: 'var(--chart-2)', }, } satisfies import('@/components/ui/chart').ChartConfig; const chartData = [ { month: 'January', desktop: 186, mobile: 80 }, { month: 'February', desktop: 305, mobile: 200 }, { month: 'March', desktop: 237, mobile: 120 }, ]; export function BarChartDemo() { return ( value.slice(0, 3)} /> } /> ); } ``` #### ChartConfig with Custom Colors You can define custom colors directly in the configuration: ```tsx const chartConfig = { visitors: { label: 'Visitors', color: '#2563eb', // Custom hex color theme: { light: '#2563eb', dark: '#60a5fa', }, }, sales: { label: 'Sales', color: 'var(--chart-1)', // CSS variable theme: { light: 'oklch(0.646 0.222 41.116)', dark: 'oklch(0.696 0.182 281.41)', }, }, } satisfies import('@/components/ui/chart').ChartConfig; ``` #### CSS Variables for Charts Add chart color variables to your `globals.css`: ```css @layer base { :root { /* Chart colors */ --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.546 0.198 38.228); --chart-4: oklch(0.596 0.151 343.253); --chart-5: oklch(0.546 0.158 49.157); } .dark { --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.698 0.141 24.311); --chart-4: oklch(0.676 0.172 171.196); --chart-5: oklch(0.578 0.192 302.85); } } ``` #### Line Chart Example ```tsx import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts'; import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart'; const chartConfig = { price: { label: 'Price', color: 'var(--chart-1)', }, } satisfies import('@/components/ui/chart').ChartConfig; const chartData = [ { month: 'January', price: 186 }, { month: 'February', price: 305 }, { month: 'March', price: 237 }, { month: 'April', price: 203 }, { month: 'May', price: 276 }, ]; export function LineChartDemo() { return ( `$${value}`} /> } /> ); } ``` #### Area Chart Example ```tsx import { Area, AreaChart, XAxis, YAxis } from 'recharts'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent, } from '@/components/ui/chart'; const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)' }, mobile: { label: 'Mobile', color: 'var(--chart-2)' }, } satisfies import('@/components/ui/chart').ChartConfig; export function AreaChartDemo() { return ( } /> } /> ); } ``` #### Pie Chart Example ```tsx import { Pie, PieChart } from 'recharts'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent, } from '@/components/ui/chart'; const chartConfig = { chrome: { label: 'Chrome', color: 'var(--chart-1)' }, safari: { label: 'Safari', color: 'var(--chart-2)' }, firefox: { label: 'Firefox', color: 'var(--chart-3)' }, } satisfies import('@/components/ui/chart').ChartConfig; const pieData = [ { browser: 'Chrome', visitors: 275, fill: 'var(--color-chrome)' }, { browser: 'Safari', visitors: 200, fill: 'var(--color-safari)' }, { browser: 'Firefox', visitors: 187, fill: 'var(--color-firefox)' }, ]; export function PieChartDemo() { return ( } /> } /> ); } ``` #### ChartTooltipContent Props | Prop | Type | Default | Description | | --------------- | --------------------------- | ------- | --------------------- | | `labelKey` | string | "label" | Key for tooltip label | | `nameKey` | string | "name" | Key for tooltip name | | `indicator` | "dot" \| "line" \| "dashed" | "dot" | Indicator style | | `hideLabel` | boolean | false | Hide label | | `hideIndicator` | boolean | false | Hide indicator | #### Accessibility Enable keyboard navigation and screen reader support: ```tsx ... ``` This adds: - Keyboard arrow key navigation - ARIA labels for chart elements - Screen reader announcements for data values ## Customization ### Theming with CSS Variables shadcn/ui uses CSS variables for theming. Configure in `globals.css`: ```css @layer base { :root { --background: 0 0% 100%; --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%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; /* ... other dark mode variables */ } } ``` ### Customizing Components Since you own the code, customize directly: ```tsx // components/ui/button.tsx import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors', { 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', // Add custom variant custom: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', // Add custom size xl: 'h-14 rounded-md px-10 text-lg', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( ); }, ); Button.displayName = 'Button'; export { Button, buttonVariants }; ``` ## Next.js Integration ### App Router Setup For Next.js 13+ with App Router, ensure components use `"use client"` directive: ```tsx // src/components/ui/button.tsx 'use client'; import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; // ... rest of component ``` ### Layout Integration Add the Toaster to your root layout: ```tsx // app/layout.tsx import { Toaster } from '@/components/ui/toaster'; import './globals.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Server Components When using shadcn/ui components in Server Components, wrap them in a Client Component: ```tsx // app/dashboard/page.tsx import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ButtonClient } from '@/components/ui/button-client'; export default function DashboardPage() { return (
Dashboard Interactive Button
); } ``` ```tsx // src/components/ui/button-client.tsx 'use client'; import { Button } from './button'; export function ButtonClient(props: React.ComponentProps) { return