Ai shadcn-ui
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.
git clone https://github.com/wpank/ai
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend/shadcn-ui" ~/.claude/skills/wpank-ai-shadcn-ui && rm -rf "$T"
skills/frontend/shadcn-ui/SKILL.mdshadcn/ui Component Patterns
Expert guide for building accessible, customizable UI components with shadcn/ui.
Installation
OpenClaw / Moltbot / Clawbot
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
# 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
cnMerges Tailwind classes with conflict resolution — used in every component:
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:
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
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.
npx shadcn@latest add form input select checkbox textarea
"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
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
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
// 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
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:
@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:
// 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 | | , , |
| Input | | Standard HTML input props |
| Form | | React Hook Form + Zod integration |
| Card | | Header, Content, Footer composition |
| Dialog | | Modal with trigger pattern |
| Sheet | | Slide-over panel, prop |
| Select | | Accessible dropdown |
| Toast | | |
| Table | | Header, Body, Row, Cell composition |
| Tabs | | , trigger/content pairs |
| Accordion | | |
| Command | | Command palette / search |
| Dropdown Menu | | Context menus, action menus |
| Menubar | | Application menus with shortcuts |
Next.js Integration
App Router Setup
For Next.js 13+ with App Router, ensure interactive components use
"use client":
// 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:
// 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
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 |
pattern | Use Radix Slot to render as different elements |
| Server Components | Most shadcn/ui components need |
| 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: |
| 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