shadcn
SKILL.md
Shadcn/ui Component Library Skill
Overview
Shadcn/ui provides copy-paste components built on Radix UI primitives and Tailwind CSS. Components are added to your project via CLI, not installed as a dependency.
# Add components
bunx --bun shadcn@latest add button card dialog form table
# Add multiple
bunx --bun shadcn@latest add button card input label
Core Components
Button
import { Button } from "@/components/ui/button"
// Variants
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><IconPlus /></Button>
// With icon
<Button>
<IconPlus className="mr-2 h-4 w-4" />
Add Item
</Button>
// Loading state
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
// As child (render as different element)
<Button asChild>
<Link to="/dashboard">Go to Dashboard</Link>
</Button>
Input & Label
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="name@example.com"
/>
</div>
// With icon
<div className="relative">
<IconSearch className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input className="pl-10" placeholder="Search..." />
</div>
// Disabled
<Input disabled value="Cannot edit" />
Card
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description text</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>
// Clickable card
<Card className="cursor-pointer hover:bg-accent transition-colors">
<CardHeader>
<CardTitle>Clickable Card</CardTitle>
</CardHeader>
</Card>
Select
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectGroup,
SelectLabel,
} from "@/components/ui/select"
<Select value={value} onValueChange={setValue}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
Checkbox & Switch
import { Checkbox } from "@/components/ui/checkbox"
import { Switch } from "@/components/ui/switch"
// Checkbox
<div className="flex items-center space-x-2">
<Checkbox id="terms" checked={checked} onCheckedChange={setChecked} />
<Label htmlFor="terms">Accept terms</Label>
</div>
// Switch
<div className="flex items-center space-x-2">
<Switch id="notifications" checked={enabled} onCheckedChange={setEnabled} />
<Label htmlFor="notifications">Enable notifications</Label>
</div>
Dialogs & Overlays
Dialog (Modal)
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
function EditUserDialog({ user }: { user: User }) {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Edit User</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue={user.name} />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={() => setOpen(false)}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
AlertDialog (Confirmation)
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete
the item from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Sheet (Side Panel)
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
SheetFooter,
SheetClose,
} from "@/components/ui/sheet"
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Settings</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[400px]">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
<SheetDescription>Configure your preferences</SheetDescription>
</SheetHeader>
<div className="py-4">
{/* Settings content */}
</div>
<SheetFooter>
<SheetClose asChild>
<Button>Save</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
// Sides: "top" | "bottom" | "left" | "right"
Drawer (Mobile-friendly)
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
DrawerFooter,
DrawerClose,
} from "@/components/ui/drawer"
<Drawer>
<DrawerTrigger asChild>
<Button>Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div className="p-4">
{/* Drawer content */}
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
Popover
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium">Dimensions</h4>
<p className="text-sm text-muted-foreground">
Set the dimensions for the layer.
</p>
</div>
{/* Form fields */}
</div>
</PopoverContent>
</Popover>
Command Palette
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command"
function CommandMenu() {
const [open, setOpen] = useState(false)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem onSelect={() => navigate("/dashboard")}>
<IconLayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</CommandItem>
<CommandItem onSelect={() => navigate("/settings")}>
<IconSettings className="mr-2 h-4 w-4" />
Settings
<CommandShortcut>⌘S</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
<CommandItem onSelect={handleCreateNew}>
<IconPlus className="mr-2 h-4 w-4" />
Create New
<CommandShortcut>⌘N</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
)
}
Dropdown Menu
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu"
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<IconDotsVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleEdit}>
<IconEdit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDuplicate}>
<IconCopy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={handleDelete}
>
<IconTrash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Tabs
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent>{/* Content */}</CardContent>
</Card>
</TabsContent>
<TabsContent value="analytics">{/* Analytics content */}</TabsContent>
<TabsContent value="settings">{/* Settings content */}</TabsContent>
</Tabs>
Toast Notifications (Sonner)
import { toast } from "sonner"
// Success
toast.success("Profile updated successfully")
// Error
toast.error("Failed to save changes")
// With description
toast("Event created", {
description: "Your event has been scheduled for tomorrow at 3pm",
})
// With action
toast("File uploaded", {
action: {
label: "View",
onClick: () => navigate("/files"),
},
})
// Promise toast
toast.promise(saveData(), {
loading: "Saving...",
success: "Data saved!",
error: "Could not save data",
})
// Custom duration
toast("Quick message", { duration: 2000 })
// Dismissible
const toastId = toast("Processing...")
// Later: toast.dismiss(toastId)
Data Table Pattern
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useReactTable, flexRender } from "@tanstack/react-table"
function DataTable<TData>({
columns,
data,
}: {
columns: ColumnDef<TData>[]
data: TData[]
}) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
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>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
Form with React Hook Form
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
})
function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
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="johndoe" {...field} />
</FormControl>
<FormDescription>
This is 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>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Utility: cn() Function
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Usage - merges Tailwind classes intelligently
<div className={cn(
"flex items-center p-4",
isActive && "bg-primary text-primary-foreground",
className
)}>
Skeleton Loading
import { Skeleton } from "@/components/ui/skeleton"
function UserCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[150px]" />
</div>
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
)
}
Avatar
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar>
<AvatarImage src={user.avatarUrl} alt={user.name} />
<AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
// Sizes with className
<Avatar className="h-6 w-6"> {/* Small */}
<Avatar className="h-10 w-10"> {/* Default */}
<Avatar className="h-16 w-16"> {/* Large */}
Badge
import { Badge } from "@/components/ui/badge"
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Destructive</Badge>
// Custom colors
<Badge className="bg-green-500 hover:bg-green-600">Active</Badge>
ScrollArea
import { ScrollArea } from "@/components/ui/scroll-area"
<ScrollArea className="h-[400px] w-full rounded-md border p-4">
{items.map((item) => (
<div key={item.id} className="py-2">
{item.name}
</div>
))}
</ScrollArea>
// Horizontal
<ScrollArea className="w-full whitespace-nowrap">
<div className="flex space-x-4">
{items.map((item) => <ItemCard key={item.id} item={item} />)}
</div>
</ScrollArea>
Tooltip
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
// Wrap app in TooltipProvider
<TooltipProvider>
<App />
</TooltipProvider>
// Usage
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<IconHelp className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Help information here</p>
</TooltipContent>
</Tooltip>
Official References
- https://ui.shadcn.com/docs/installation
- https://ui.shadcn.com/docs/cli
- https://ui.shadcn.com/docs/changelog
- https://ui.shadcn.com/docs/forms/tanstack-form
- https://www.radix-ui.com/primitives/docs/overview/introduction
- https://tailwindcss.com/docs/installation
Shared Styleguide Baseline
- Use shared styleguides for generic language/framework rules to reduce duplication in this skill.
- General Principles
- Tailwind and Shadcn
- React
- Keep this skill focused on tool-specific workflows, edge cases, and integration details.
Weekly Installs
3
Repository
cofin/flowGitHub Stars
6
First Seen
Feb 28, 2026
Security Audits
Installed on
opencode3
claude-code3
github-copilot3
codex3
kimi-cli3
gemini-cli3