---
name: shadcn-ui
description: Use when building React UIs with component libraries, implementing forms, dialogs, navigation, or data display. Use when user mentions shadcn, Radix UI, Base UI, or asks about accessible React components. Proactively suggest when building UI that would benefit from pre-built accessible components with Tailwind CSS styling.
---
# ShadCN/UI Expert Skill
## Overview
ShadCN/UI is a collection of beautifully-designed, accessible components built with TypeScript, Tailwind CSS, and headless UI primitives (Base UI or Radix UI). Unlike traditional component libraries, ShadCN uses a **copy-paste model** - components are copied into YOUR project, giving you full ownership and customization control.
**Core Principle:** You own the code. Components live in your project (typically `src/components/ui/`), not in node_modules. This fundamentally changes how you think about customization - edit the source directly.
## When to Use
### Choose ShadCN When:
- Building React apps needing accessible, polished components
- You want full control over component code (not locked to library updates)
- Using Tailwind CSS for styling
- Need consistent design system with customization flexibility
- Building forms with validation (React Hook Form + Zod integration)
### Base Library Selection
During project creation (`shadcn create`), choose your base primitives:
| Base Library | Choose When |
|--------------|-------------|
| **Base UI** | Prefer MUI ecosystem, need unstyled primitives with strong React patterns |
| **Radix UI** | Want extensive primitive catalog, strong accessibility defaults |
Example preset with Base UI:
```bash
pnpm dlx shadcn@latest create --preset "https://ui.shadcn.com/init?base=base&style=nova&baseColor=stone" --template vite
```
### Choose Alternatives When:
- **Not using Tailwind** - ShadCN is Tailwind-first
- **Want batteries-included** - Use Chakra UI, Mantine, or MUI
- **Vue/Svelte project** - Use shadcn-vue or shadcn-svelte ports
- **Need zero styling opinions** - Use Base UI or Radix primitives directly
## Component Selection
```
Need user input?
→ Form & Input category (Input, Select, Checkbox, Form)
Need to show/hide content?
→ Triggered by click/hover? → Overlays (Dialog, Popover, Tooltip, Sheet)
→ User-controlled toggle? → Layout (Accordion, Tabs, Collapsible)
Need to display data?
→ Single item → Card
→ Multiple items → Table or Data Table
Need feedback?
→ Temporary → Toast (Sonner)
→ Persistent → Alert
```
Refer to `llms.txt` in this skill directory for the full component index with documentation links.
## Core Patterns
### The "Own Your Code" Model
ShadCN copies components into YOUR project. This means:
- Full customization - edit the source directly
- No version lock-in - you control updates
- Tree-shaking friendly - only what you use
- You maintain the code - breaking changes are your responsibility
### Component Composition
ShadCN components are composable primitives, not monolithic widgets:
```tsx
// Anti-pattern: Expecting a single "FormInput" component
// ShadCN pattern: Compose primitives
Email
```
### Form Pattern (React Hook Form + Zod)
```tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
const schema = z.object({
email: z.string().email("Invalid email address"),
name: z.string().min(2, "Name must be at least 2 characters"),
})
type FormData = z.infer
function ContactForm() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { email: "", name: "" },
})
const onSubmit = (data: FormData) => {
console.log(data)
}
return (
)
}
```
### Dialog with Form Pattern
```tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { useState } from "react"
function DialogWithForm() {
const [open, setOpen] = useState(false)
const handleSuccess = () => {
setOpen(false) // Close dialog on successful submit
}
return (
)
}
```
### Toast Notifications (Sonner)
```tsx
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
function ToastExample() {
return (