Shadcn UI
VerifiedBuild accessible, customizable UIs with shadcn/ui, Radix UI, and Tailwind CSS. Use when setting up shadcn/ui, installing components, building forms with React Hook Form + Zod, customizing themes, or implementing component patterns.
Install
Claude Code
Add to .claude/skills/ About This Skill
# shadcn/ui Component Patterns
Expert guide for building accessible, customizable UI components with shadcn/ui.
Installation
OpenClaw / Moltbot / Clawbot
```bash npx clawhub@latest install shadcn-ui ```
WHEN
- 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
What is shadcn/ui?
A collection of reusable components you copy into your project — not an npm package. You own the code. Built on Radix UI (accessibility) and Tailwind CSS (styling).
Quick Start
```bash # New Next.js project npx create-next-app@latest my-app --typescript --tailwind --eslint --app cd my-app npx shadcn@latest init
# Install components npx shadcn@latest add button input form card dialog select toast npx shadcn@latest add --all # or install everything ```
Core Concepts
The `cn` Utility
Merges Tailwind classes with conflict resolution — used in every component:
```tsx import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ```
Class Variance Authority (CVA)
Manages component variants — the pattern behind every shadcn/ui component:
```tsx import { cva, type VariantProps } from "class-variance-authority"
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", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default" }, } ) ```
Essential Components
Button
```tsx import { Button } from "@/components/ui/button" import { Loader2 } from "lucide-react"
// Variants: default | destructive | outline | secondary | ghost | link // Sizes: default | sm | lg | icon <Button variant="outline" size="sm">Click me</Button>
// Loading state <Button disabled> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Please wait </Button>
// As link (uses Radix Slot) <Button asChild> <a href="/dashboard">Go to Dashboard</a> </Button> ```
Forms with Validation
The standard pattern: Zod schema + React Hook Form + shadcn Form components.
```bash npx shadcn@latest add form input select checkbox textarea ```
```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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
const formSchema = z.object({ username: z.string().min(2, "Username must be at least 2 characters."), email: z.string().email("Please enter a valid email."), role: z.enum(["admin", "user", "guest"]), })
export function ProfileForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { username: "", email: "", role: "user" }, })
function onSubmit(values: z.infer<typeof formSchema>) { console.log(values) }
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <FormField control={form.control} name="username" render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl><Input placeholder="shadcn" {...field} /></FormControl> <FormDescription>Your public display name.</FormDescription> <FormMessage /> </FormItem> )} />
<FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl><Input type="email" {...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="user">User</SelectItem> <SelectItem value="guest">Guest</SelectItem> </SelectContent> </Select> <FormMessage /> </FormItem> )} />
<Button type="submit">Submit</Button> </form> </Form> ) } ```
Dialog & Sheet
```tsx import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"
// Modal dialog <Dialog> <DialogTrigger asChild><Button variant="outline">Edit profile</Button></DialogTrigger> <DialogContent className="sm:max-w-[425px]"> <DialogHeader> <DialogTitle>Edit profile</DialogTitle> <DialogDescription>Make changes here. Click save when done.</DialogDescription> </DialogHeader> <div className="grid gap-4 py-4">{/* form fields */}</div> <DialogFooter><Button type="submit">Save changes</Button></DialogFooter> </DialogContent> </Dialog>
// Slide-over panel (side: "left" | "right" | "top" | "bottom") <Sheet> <SheetTrigger asChild><Button variant="outline">Open</Button></SheetTrigger> <SheetContent side="right"> <SheetHeader><SheetTitle>Settings</SheetTitle></SheetHeader> {/* content */} </SheetContent> </Sheet> ```
Card
```tsx import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"
<Card className="w-[350px]"> <CardHeader> <CardTitle>Create project</CardTitle> <CardDescription>Deploy your new project in one-click.</CardDescription> </CardHeader> <CardContent> <div className="grid w-full items-center gap-4"> <div className="flex flex-col space-y-1.5"> <Label htmlFor="name">Name</Label> <Input id="name" placeholder="Project name" /> </div> </div> </CardContent> <CardFooter className="flex justify-between"> <Button variant="outline">Cancel</Button> <Button>Deploy</Button> </CardFooter> </Card> ```
Toast Notifications
```tsx // 1. Add Toaster to root layout import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}<Toaster /></body> </html> ) }
// 2. Use toast in components import { useToast } from "@/components/ui/use-toast" import { ToastAction } from "@/components/ui/toast"
const { toast } = useToast()
toast({ title: "Success", description: "Changes saved." })
toast({ variant: "destructive", title: "Error", description: "Something went wrong.", action: <ToastAction altText="Try again">Try again</ToastAction>, }) ```
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" }, ]
<Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead>Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map((invoice) => ( <TableRow key={invoice.invoice}> <TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell>{invoice.status}</TableCell> <TableCell>{invoice.method}</TableCell> <TableCell className="text-right">{invoice.amount}</TableCell> </TableRow> ))} </TableBody> </Table> ```
Theming
shadcn/ui uses CSS variables in HSL format. 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%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --destructive: 0 84.2% 60.2%; --border: 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%; /* ... mirror all variables for dark mode */ } } ```
Colors reference as `hsl(var(--primary))` in Tailwind config. Change the CSS variables to retheme the entire app.
Customizing Components
Since you own the code, modify components directly:
```tsx // Add a custom variant to button.tsx const buttonVariants = cva("...", { variants: { variant: { // ... existing variants gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white", }, size: { // ... existing sizes xl: "h-14 rounded-md px-10 text-lg", }, }, }) ```
Component Reference
| Component | Install | Key Props | |-----------|---------|-----------| | Button | `add button` | `variant`, `size`, `asChild` | | Input | `add input` | Standard HTML input props | | Form | `add form` | React Hook Form + Zod integration | | Card | `add card` | Header, Content, Footer composition | | Dialog | `add dialog` | Modal with trigger pattern | | Sheet | `add sheet` | Slide-over panel, `side` prop | | Select | `add select` | Accessible dropdown | | Toast | `add toast` | `variant: "default" \| "destructive"` | | Table | `add table` | Header, Body, Row, Cell composition | | Tabs | `add tabs` | `defaultValue`, trigger/content pairs | | Accordion | `add accordion` | `type: "single" \| "multiple"` | | Command | `add command` | Command palette / search | | Dropdown Menu | `add dropdown-menu` | Context menus, action menus | | Menubar | `add menubar` | Application menus with shortcuts |
Next.js Integration
App Router Setup
For Next.js 13+ with App Router, ensure interactive components use `"use client"`:
```tsx // src/components/ui/button.tsx "use client"
import * as React from "react" import { Slot } from "@radix-ui/react-slot" // ... 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 ( <html lang="en" suppressHydrationWarning> <body className="min-h-screen bg-background font-sans antialiased"> {children} <Toaster /> </body> </html> ) } ```
Server Components
Most shadcn/ui components need `"use client"`. For Server Components, wrap them in a client component or use them in client component children.
CLI Reference
```bash npx shadcn@latest init # Initialize project npx shadcn@latest add [component] # Add specific component npx shadcn@latest add --all # Add all components npx shadcn@latest diff # Show upstream changes ```
Best Practices
| Practice | Details | |----------|---------| | Use TypeScript | All components ship with full type definitions | | Zod for validation | Pair with React Hook Form for type-safe forms | | `asChild` pattern | Use Radix Slot to render as different elements | | Server Components | Most shadcn/ui components need `"use client"` | | Consistent structure | Follow the existing component patterns when customizing | | Accessibility | Radix primitives handle ARIA; don't override without reason | | CSS variables | Theme via variables, not by editing component classes | | Tree-shaking | Only install components you need — they're independent |
NEVER Do
| Never | Why | Instead | |-------|-----|---------| | Install shadcn as npm package | It's not a package — it's source code you own | Use CLI: `npx shadcn@latest add` | | Override ARIA attributes | Radix handles accessibility correctly | Trust the primitives | | Use inline styles for theming | Defeats the design system | Modify CSS variables | | Copy components from docs manually | May miss dependencies | Use CLI for proper installation | | Mix component styles | Creates inconsistency | Follow CVA variant pattern |
References
- Learning Guide — progression from basics to advanced patterns
- Extended Components — Terminal, Dock, Charts, animations, custom hooks
- Official Docs | Radix UI | React Hook Form | Zod
Use Cases
- Create responsive HTML email templates using React Email components
- Build modern UI components using shadcn/ui with Tailwind CSS
- Implement custom React hooks for real-time data and state management
- Follow React best practices for component design, state management, and performance
- Build production-ready frontend applications with proper patterns and testing
Pros & Cons
Pros
- + Solid adoption with 800+ downloads
- + Clean CLI interface integrates well with automation pipelines and AI agents
- + Follows modern frontend best practices and established patterns
- + Production-ready code examples reduce implementation time
Cons
- - Framework-specific — may not apply to projects using different technology stacks
- - Opinionated patterns may conflict with existing project conventions
Frequently Asked Questions
What does Shadcn UI do?
Build accessible, customizable UIs with shadcn/ui, Radix UI, and Tailwind CSS. Use when setting up shadcn/ui, installing components, building forms with React Hook Form + Zod, customizing themes, or implementing component patterns.
What platforms support Shadcn UI?
Shadcn UI is available on Claude Code, OpenClaw.
What are the use cases for Shadcn UI?
Create responsive HTML email templates using React Email components. Build modern UI components using shadcn/ui with Tailwind CSS. Implement custom React hooks for real-time data and state management.
Stay Updated on Agent Skills
Get weekly curated skills + safety alerts