| context | fork |
| name | library-shadcn |
| description | Provides shadcn/ui component patterns for React including installation, theming, dark mode, CVA variants, and accessibility with Radix UI primitives. Use when the user asks about building UIs with shadcn/ui, setting up component libraries, customizing themes or dark mode, or composing form patterns. Triggers: shadcn, shadcn/ui, radix, tailwind components, ui library, component library, dark mode. |
| lang | ["en"] |
| level | 2 |
| triggers | ["shadcn","shadcn/ui","radix","tailwind components","ui library","component library","dark mode"] |
| agents | ["frontend-developer","architect"] |
| tokens | ~4K |
| category | library |
| platforms | ["claude-code","gemini-cli","codex-cli","cursor"] |
| sources | ["https://ui.shadcn.com/docs","https://www.radix-ui.com/primitives/docs/overview/introduction"] |
| version | 1.0.0 |
| lastVerified | 2026-06-08 |
| source_hash | ab45ed51 |
| whenNotToUse | Non-React frontend stacks (Vue, Svelte, Angular) or projects that do not use Tailwind CSS, where shadcn/ui component patterns cannot be directly applied. |
shadcn/ui Component Patterns
Contents
When This Skill Applies
- Building React UIs with shadcn/ui components
- Setting up theming and dark mode support
- Composing complex UI patterns from primitive components
- Customizing component variants with Tailwind CSS
- Ensuring accessibility compliance in component usage
- Integrating shadcn/ui with Next.js, Remix, or Vite projects
Core Guidance
1. Installation & Setup
Initialize in a project:
npx shadcn@latest init
Add individual components (not installed as dependency -- copied into your project):
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
Key Principle: shadcn/ui copies component source code into your project. You own the code and can customize freely. This is not a traditional npm package.
Project Structure:
src/
components/
ui/ # shadcn/ui primitives (auto-generated)
button.tsx
dialog.tsx
input.tsx
custom/ # Your compositions using ui primitives
user-form.tsx
data-table.tsx
lib/
utils.ts # cn() utility for class merging
2. Theming System
CSS Variables (defined in globals.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%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--border: 214.3 31.8% 91.4%;
--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%;
}
}
Theme Customization Rules:
- Modify CSS variables, not component source (when possible)
- Use HSL format without the
hsl() wrapper for variable values
- All shadcn components reference these variables via Tailwind classes
- Generate custom themes at ui.shadcn.com/themes
3. Dark Mode Implementation
Next.js with next-themes:
import { ThemeProvider } from 'next-themes'
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{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-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
4. Component Composition Patterns
Form with Validation (React Hook Form + Zod):
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
const schema = z.object({
email: z.string().email(),
name: z.string().min(2).max(50),
})
export function UserForm() {
const form = useForm({ resolver: zodResolver(schema) })
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="user@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Data Table Pattern:
import { DataTable } from '@/components/ui/data-table'
import { columns } from './columns'
Dialog with Form:
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Update your information below.</DialogDescription>
</DialogHeader>
<UserForm />
</DialogContent>
</Dialog>
5. Component Variants with CVA
Class Variance Authority (used internally by shadcn):
import { cva, type VariantProps } from 'class-variance-authority'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive text-destructive-foreground',
outline: 'text-foreground',
},
},
defaultVariants: { variant: 'default' },
}
)
Extending Components:
6. Tailwind CSS Integration
The cn() Utility (essential for class merging):
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Responsive Design:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
{/* Content */}
</Card>
</div>
7. Accessibility Guidelines
Built-in Accessibility (via Radix UI primitives):
- Keyboard navigation (Tab, Enter, Escape, Arrow keys)
- Focus management and focus trapping in modals
- ARIA attributes automatically applied
- Screen reader announcements for dynamic content
Your Responsibilities:
- Always provide
DialogTitle and DialogDescription for dialogs
- Use
sr-only class for icon-only buttons: <span className="sr-only">Close</span>
- Set
aria-label on interactive elements without visible text
- Maintain color contrast ratios (4.5:1 for normal text, 3:1 for large text)
- Test with keyboard navigation (no mouse)
- Verify with screen reader (VoiceOver, NVDA)
Focus Management:
8. Common Component Patterns
| Pattern | Components Used | Use Case |
|---|
| CRUD Table | DataTable + Dialog + Form | Admin panels, data management |
| Command Palette | Command + CommandInput + CommandList | Search, navigation |
| Settings Page | Tabs + Form + Switch + Select | User preferences |
| Dashboard | Card + Chart + Table + Badge | Analytics, overview |
| Auth Pages | Card + Form + Input + Button | Login, signup |
| Navigation | NavigationMenu + Sheet (mobile) | Site header, sidebar |
Anti-Patterns
- Installing shadcn/ui as an npm package (it copies source files, not a dependency)
- Overriding component styles with
!important instead of modifying source
- Not using the
cn() utility for conditional classes
- Forgetting
asChild prop when wrapping triggers with custom elements
- Missing
DialogTitle causing accessibility warnings
- Hardcoding colors instead of using CSS variable tokens
Quick Reference
Add Component: npx shadcn@latest add [component-name]
Class Merging: Always use cn() for conditional and merged classes
Theming: Modify CSS variables in globals.css, not component source
Dark Mode: next-themes + attribute="class" + .dark CSS scope
Forms: React Hook Form + Zod + shadcn Form components
Accessibility: Radix handles ARIA; you handle labels, contrast, and keyboard testing
Rationalizations
The following table captures common excuses agents make to skip the rigor of this skill, paired with factual rebuttals.
| Excuse | Rebuttal |
|---|
| "I will build my own components" | shadcn gives you accessible Radix primitives โ reinventing means reinventing a11y bugs |
| "copy-paste components are a mess" | shadcn is not a dependency โ owning the source is the feature, not the bug |
| "CVA variants are overkill" | variants centralize style decisions; without them you get prop-drilling className everywhere |
| "dark mode with Tailwind is enough" | semantic CSS variables beat dark: prefixes at scale โ use shadcn tokens |
| "I will skip Radix and use divs" | divs are not accessible by default โ Radix gives you keyboard nav and ARIA for free |