
Shadcn Ui
- 3.3k installs
- 946 repo stars
- Updated July 2, 2026
- jezweb/claude-skills
How to install, configure, customize, and combine shadcn/ui components in a React project with an established theme.
About
This skill installs and configures shadcn/ui components into React projects after theme infrastructure is established via tailwind-theme-builder. It provides dependency-ordered installation sequences for foundation components (button, input, card) and feature components (forms, tables, dialogs, navigation). Guides customization through semantic CSS tokens, documents known gotchas (Radix Select empty strings, React Hook Form null handling, Lucide tree-shaking, Dialog width overrides), and assembles components into working recipes including contact forms, data tables, modal CRUD interfaces, and settings pages. Prerequisites include CSS variables, components.json, and cn() utility setup.
- Dependency-ordered installation: foundation first, then feature components
- Known gotchas documented: Radix Select values, React Hook Form spreading, Lucide dynamic imports, Dialog width breakpoin
- Component recipes for forms, data tables, CRUD modals, and navigation
- Customization via semantic tokens and variant extension in component files
- External dependencies tracked: react-hook-form, zod, sonner, @tanstack/react-table, cmdk
Shadcn Ui by the numbers
- 3,260 all-time installs (skills.sh)
- +36 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #149 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jezweb/claude-skills --skill shadcn-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.3k |
|---|---|
| repo stars | ★ 946 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 2, 2026 |
| Repository | jezweb/claude-skills ↗ |
What it does
Install and customize shadcn/ui components in themed React projects with forms, data tables, and navigation patterns.
Who is it for?
React projects with established Tailwind theme infrastructure; developers building forms, data tables, admin interfaces, or navigation systems.
Skip if: Projects without theme setup; non-React frameworks; static HTML; projects avoiding component libraries.
When should I use this skill?
Adding UI components after tailwind-theme-builder setup; building forms with validation; creating data tables; implementing navigation; setting up modal dialogs.
What you get
Developer can install shadcn components in correct order, avoid documented gotchas, customize with semantic tokens, and assemble components into complete working UI patterns.
- component install commands
- tsx snippets
- variant and size reference
By the numbers
- Covers ~15 most-used shadcn/ui components
- Button documents 6 variants and 4 sizes
Files
shadcn/ui Components
Add shadcn/ui components to a themed React project. This skill runs AFTER tailwind-theme-builder has set up CSS variables, ThemeProvider, and dark mode. It handles component installation, customisation, and combining components into working patterns.
Prerequisite: Theme infrastructure must exist (CSS variables, components.json, cn() utility). Use tailwind-theme-builder first if not set up.
Installation Order
Install components in dependency order. Foundation components first, then feature components:
Foundation (install first)
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add input label
pnpm dlx shadcn@latest add cardFeature Components (install as needed)
# Forms
pnpm dlx shadcn@latest add form # needs: react-hook-form, zod, @hookform/resolvers
pnpm dlx shadcn@latest add textarea select checkbox switch
# Feedback
pnpm dlx shadcn@latest add toast # needs: sonner
pnpm dlx shadcn@latest add alert badge
# Overlay
pnpm dlx shadcn@latest add dialog sheet popover dropdown-menu
# Data Display
pnpm dlx shadcn@latest add table # for data tables, also: @tanstack/react-table
pnpm dlx shadcn@latest add tabs separator avatar
# Navigation
pnpm dlx shadcn@latest add navigation-menu commandExternal Dependencies
| Component | Requires |
|---|---|
| Form | react-hook-form, zod, @hookform/resolvers |
| Toast | sonner |
| Data Table | @tanstack/react-table |
| Command | cmdk |
| Date Picker | date-fns (optional) |
Install external deps separately: pnpm add react-hook-form zod @hookform/resolvers
Known Gotchas
These are documented corrections that prevent common bugs:
Radix Select — No Empty Strings
// Don't use empty string values
<SelectItem value="">All</SelectItem> // BREAKS
// Use sentinel value
<SelectItem value="__any__">All</SelectItem> // WORKS
const actual = value === "__any__" ? "" : valueReact Hook Form — Null Values
// Don't spread {...field} — it passes null which Input rejects
<Input
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>Lucide Icons — Tree-Shaking
// Don't use dynamic import — icons get tree-shaken in production
import * as LucideIcons from 'lucide-react'
const Icon = LucideIcons[iconName] // BREAKS in prod
// Use explicit map
import { Home, Users, Settings, type LucideIcon } from 'lucide-react'
const ICON_MAP: Record<string, LucideIcon> = { Home, Users, Settings }
const Icon = ICON_MAP[iconName]Dialog Width Override
// Default sm:max-w-lg won't be overridden by max-w-6xl
<DialogContent className="max-w-6xl"> // DOESN'T WORK
// Use same breakpoint prefix
<DialogContent className="sm:max-w-6xl"> // WORKSCustomising Components
shadcn components use semantic CSS tokens from your theme. To customise:
Variant extension
Add custom variants by editing the component file in src/components/ui/:
// button.tsx — add a "brand" variant
const buttonVariants = cva("...", {
variants: {
variant: {
default: "bg-primary text-primary-foreground",
brand: "bg-brand text-brand-foreground hover:bg-brand/90",
// ... existing variants
},
},
})Colour overrides
Use semantic tokens from your theme — never raw Tailwind colours:
// Don't use raw colours
<Button className="bg-blue-500"> // WRONG
// Use semantic tokens
<Button className="bg-primary"> // RIGHT
<Card className="bg-card text-card-foreground"> // RIGHTWorkflow
Step 1: Assess Needs
Determine what UI patterns the project needs:
| Need | Components |
|---|---|
| Forms with validation | Form, Input, Label, Select, Textarea, Button, Toast |
| Data display with sorting | Table, Badge, Pagination |
| Admin CRUD interface | Dialog, Form, Table, Button, Toast |
| Marketing/landing page | Card, Button, Badge, Separator |
| Settings/preferences | Tabs, Form, Switch, Select, Toast |
| Navigation | NavigationMenu (desktop), Sheet (mobile), ModeToggle |
Step 2: Install Components
Install foundation first, then feature components for the identified needs. Use the commands above.
Step 3: Build Recipes
Combine components into working patterns. See references/recipes.md for complete working examples:
- Contact Form — Form + Input + Textarea + Button + Toast
- Data Table — Table + Column sorting + Pagination + Search
- Modal CRUD — Dialog + Form + Button
- Navigation — Sheet + NavigationMenu + ModeToggle
- Settings Page — Tabs + Form + Switch + Select + Toast
Step 4: Customise
Apply project-specific colours and variants using semantic tokens from the theme.
Reference Files
| When | Read |
|---|---|
| Choosing components, install commands, props | references/component-catalogue.md |
| Building complete UI patterns | references/recipes.md |
Component Catalogue
The ~15 most-used shadcn/ui components with install commands, key props, and gotchas. Not exhaustive — see shadcn/ui docs for the full list.
Button
pnpm dlx shadcn@latest add buttonVariants: default, destructive, outline, secondary, ghost, link Sizes: default, sm, lg, icon
<Button variant="outline" size="sm" onClick={handleClick}>Save</Button>
<Button variant="ghost" size="icon"><Trash className="h-4 w-4" /></Button>
<Button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</Button>Input + Label
pnpm dlx shadcn@latest add input label<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>Note: When using with react-hook-form, don't spread {...field} — pass props individually to avoid null value issues.
Card
pnpm dlx shadcn@latest add card<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Body</CardContent>
<CardFooter>Footer</CardFooter>
</Card>Form
pnpm dlx shadcn@latest add form
pnpm add react-hook-form zod @hookform/resolversWraps react-hook-form with shadcn styling. See recipes.md for complete form examples.
Key exports: Form, FormField, FormItem, FormLabel, FormControl, FormMessage
Dialog
pnpm dlx shadcn@latest add dialog<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
</DialogHeader>
{/* content */}
<DialogFooter>
<Button onClick={() => setOpen(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>Gotcha: Override width with sm:max-w-* (must match breakpoint prefix).
Sheet
pnpm dlx shadcn@latest add sheetSide panel — commonly used for mobile navigation.
<Sheet>
<SheetTrigger asChild><Button variant="ghost" size="icon"><Menu /></Button></SheetTrigger>
<SheetContent side="left">
<SheetHeader><SheetTitle>Navigation</SheetTitle></SheetHeader>
{/* nav links */}
</SheetContent>
</Sheet>Sides: left, right, top, bottom
Table
pnpm dlx shadcn@latest add tableStatic table. For sortable/filterable data tables, also install @tanstack/react-table. See recipes.md for the data table pattern.
<Table>
<TableHeader>
<TableRow><TableHead>Name</TableHead><TableHead>Email</TableHead></TableRow>
</TableHeader>
<TableBody>
{users.map(u => (
<TableRow key={u.id}>
<TableCell>{u.name}</TableCell>
<TableCell>{u.email}</TableCell>
</TableRow>
))}
</TableBody>
</Table>Select
pnpm dlx shadcn@latest add select<Select value={value} onValueChange={setValue}>
<SelectTrigger><SelectValue placeholder="Choose..." /></SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>Gotcha: No empty string values. Use "__any__" sentinel for "All" options.
Toast (Sonner)
pnpm dlx shadcn@latest add toast
pnpm add sonnerAdd <Toaster /> to your root layout, then:
import { toast } from 'sonner'
toast.success('Saved successfully')
toast.error('Something went wrong')
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Saved!',
error: 'Failed to save',
})Tabs
pnpm dlx shadcn@latest add tabs<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>
<TabsContent value="general">General settings...</TabsContent>
<TabsContent value="security">Security settings...</TabsContent>
</Tabs>Dropdown Menu
pnpm dlx shadcn@latest add dropdown-menu<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon"><MoreHorizontal /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive" onClick={handleDelete}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Badge
pnpm dlx shadcn@latest add badgeVariants: default, secondary, outline, destructive
<Badge variant="secondary">Draft</Badge>
<Badge variant="destructive">Overdue</Badge>Switch
pnpm dlx shadcn@latest add switch<div className="flex items-center gap-2">
<Switch id="notifications" checked={enabled} onCheckedChange={setEnabled} />
<Label htmlFor="notifications">Enable notifications</Label>
</div>Separator
pnpm dlx shadcn@latest add separator<Separator /> {/* horizontal */}
<Separator orientation="vertical" className="h-6" /> {/* vertical */}Avatar
pnpm dlx shadcn@latest add avatar<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>Component Recipes
Complete working examples combining shadcn/ui components into common UI patterns.
Contact Form
Components: Form + Input + Textarea + Button + Toast
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import {
Form, FormField, FormItem, FormLabel, FormControl, FormMessage,
} from '@/components/ui/form'
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
export function ContactForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { name: '', email: '', message: '' },
})
async function onSubmit(values: z.infer<typeof schema>) {
try {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
toast.success('Message sent!')
form.reset()
} catch {
toast.error('Failed to send message')
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField control={form.control} name="name" render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input value={field.value ?? ''} onChange={field.onChange} onBlur={field.onBlur} name={field.name} ref={field.ref} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl><Input type="email" value={field.value ?? ''} onChange={field.onChange} onBlur={field.onBlur} name={field.name} ref={field.ref} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="message" render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl><Textarea value={field.value ?? ''} onChange={field.onChange} onBlur={field.onBlur} name={field.name} ref={field.ref} /></FormControl>
<FormMessage />
</FormItem>
)} />
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Sending...' : 'Send'}
</Button>
</form>
</Form>
)
}Data Table
Components: Table + @tanstack/react-table + Input (search) + Button (pagination)
import { useState } from 'react'
import {
useReactTable, getCoreRowModel, getSortedRowModel,
getFilteredRowModel, getPaginationRowModel,
flexRender, type ColumnDef, type SortingState,
} from '@tanstack/react-table'
import { ArrowUpDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
interface DataTableProps<T> {
columns: ColumnDef<T>[]
data: T[]
}
export function DataTable<T>({ columns, data }: DataTableProps<T>) {
const [sorting, setSorting] = useState<SortingState>([])
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({
data, columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<div className="space-y-4">
<Input placeholder="Search..." value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)} className="max-w-sm" />
<Table>
<TableHeader>
{table.getHeaderGroups().map(hg => (
<TableRow key={hg.id}>
{hg.headers.map(h => (
<TableHead key={h.id}>
{h.isPlaceholder ? null : (
<Button variant="ghost" onClick={h.column.getToggleSortingHandler()}>
{flexRender(h.column.columnDef.header, h.getContext())}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map(row => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length} result(s)
</span>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
</div>
</div>
</div>
)
}Modal CRUD
Components: Dialog + Form + Button (create/edit in a modal)
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface Item { id: string; name: string; email: string }
export function CrudModal({ item, open, onOpenChange, onSave }: {
item?: Item // undefined = create, defined = edit
open: boolean
onOpenChange: (open: boolean) => void
onSave: (data: Omit<Item, 'id'>) => Promise<void>
}) {
const [name, setName] = useState(item?.name ?? '')
const [email, setEmail] = useState(item?.email ?? '')
const [saving, setSaving] = useState(false)
async function handleSave() {
setSaving(true)
try {
await onSave({ name, email })
toast.success(item ? 'Updated!' : 'Created!')
onOpenChange(false)
} catch {
toast.error('Failed to save')
} finally {
setSaving(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{item ? 'Edit' : 'Create'} Item</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={handleSave} disabled={saving}>{saving ? 'Saving...' : 'Save'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Responsive Navigation
Components: NavigationMenu (desktop) + Sheet (mobile) + ModeToggle
import { useState } from 'react'
import { Menu } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { ModeToggle } from '@/components/mode-toggle'
const navLinks = [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about' },
{ label: 'Contact', href: '/contact' },
]
export function Navigation() {
return (
<header className="border-b">
<div className="container flex h-14 items-center justify-between">
<span className="font-bold">Logo</span>
{/* Desktop nav */}
<nav className="hidden md:flex items-center gap-6">
{navLinks.map(link => (
<a key={link.href} href={link.href} className="text-sm text-muted-foreground hover:text-foreground transition-colors">
{link.label}
</a>
))}
<ModeToggle />
</nav>
{/* Mobile nav */}
<div className="flex md:hidden items-center gap-2">
<ModeToggle />
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon"><Menu className="h-5 w-5" /></Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader><SheetTitle>Menu</SheetTitle></SheetHeader>
<nav className="flex flex-col gap-4 mt-4">
{navLinks.map(link => (
<a key={link.href} href={link.href} className="text-lg">{link.label}</a>
))}
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
)
}Settings Page
Components: Tabs + Form + Switch + Select + Toast
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Separator } from '@/components/ui/separator'
export function SettingsPage() {
return (
<div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">Settings</h1>
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-6 mt-4">
<div className="space-y-2">
<Label htmlFor="name">Display Name</Label>
<Input id="name" defaultValue="Alex" />
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select defaultValue="australia-sydney">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="australia-sydney">Australia/Sydney</SelectItem>
<SelectItem value="utc">UTC</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<Button onClick={() => toast.success('Settings saved')}>Save</Button>
</TabsContent>
<TabsContent value="notifications" className="space-y-4 mt-4">
<div className="flex items-center justify-between">
<div>
<Label>Email notifications</Label>
<p className="text-sm text-muted-foreground">Receive email updates</p>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<Label>Marketing emails</Label>
<p className="text-sm text-muted-foreground">Receive promotional content</p>
</div>
<Switch />
</div>
</TabsContent>
</Tabs>
</div>
)
}Related skills
How it compares
Use shadcn-ui for fast in-editor recall of top components; open official shadcn/ui docs when you need rare primitives or theming deep dives.
FAQ
How many shadcn/ui components does shadcn-ui cover?
shadcn-ui documents roughly 15 of the most-used shadcn/ui components with install commands and examples. The full shadcn/ui library is larger; see ui.shadcn.com for components outside this catalogue.
Which package manager does shadcn-ui use for installs?
shadcn-ui examples use pnpm dlx shadcn@latest add followed by the component name, such as button or input label pairs, matching the official shadcn CLI workflow.
Is Shadcn Ui safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.