Shadcn UI
"shadcn/ui: copy-paste components, CLI installation, theming with CSS variables, dark mode, component customization, form integration, data tables, command palette"
shadcn/ui is not a component library you install as a dependency. It is a collection of reusable components that you copy and paste into your project. You own the code. You can customize every component to fit your needs. Components are built on top of Radix UI primitives and styled with Tailwind CSS. The CLI automates adding components, but the result is source code in your repository that you control completely. ## Key Points - Components live in your codebase, not in node_modules - Built on Radix UI for accessibility - Styled with Tailwind CSS and CSS variables for theming - Every component is a starting point, not a final product 1. **Use the CLI to add components** rather than copying code manually. The CLI resolves dependencies and sets up imports correctly. 2. **Customize at the CSS variable level** for global theme changes. Edit individual component files for structural changes. 3. **Compose components** by combining primitives. A card with a form inside uses Card, Form, Input, Button together. 4. **Keep component files in `ui/`** and build feature components that import from `ui/`. Never modify `ui/` components for feature-specific logic; wrap them instead. 5. **Use `asChild` prop** to merge shadcn component behavior onto your own elements, avoiding extra DOM nodes. 6. **Leverage `cva` (class-variance-authority)** for creating component variants that are consistent with the design system. 7. **Update components** by re-running the CLI add command. Diff the changes against your customizations. 1. **Installing shadcn/ui as an npm package** -- it is not meant to be consumed as a dependency. You copy the source code into your project.
skilldb get ui-components-services-skills/Shadcn UIFull skill: 366 linesshadcn/ui Skill
Core Philosophy
shadcn/ui is not a component library you install as a dependency. It is a collection of reusable components that you copy and paste into your project. You own the code. You can customize every component to fit your needs. Components are built on top of Radix UI primitives and styled with Tailwind CSS. The CLI automates adding components, but the result is source code in your repository that you control completely.
Key principles:
- Components live in your codebase, not in node_modules
- Built on Radix UI for accessibility
- Styled with Tailwind CSS and CSS variables for theming
- Every component is a starting point, not a final product
Setup
Project Initialization
# Initialize shadcn/ui in an existing Next.js/Vite project
npx shadcn@latest init
# Answer prompts for:
# - TypeScript: yes
# - Style: Default or New York
# - Base color: Slate, Gray, Zinc, Neutral, Stone
# - CSS variables for colors: yes
# - tailwind.config location
# - Components alias: @/components
# - Utils alias: @/lib/utils
Adding Components
# Add individual components
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
# Add multiple components at once
npx shadcn@latest add button card dialog dropdown-menu
# View all available components
npx shadcn@latest add
Project Structure
src/
components/
ui/
button.tsx # shadcn components live here
dialog.tsx
input.tsx
form.tsx
lib/
utils.ts # cn() utility function
The cn() Utility
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Key Techniques
Theming with CSS Variables
/* app/globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--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%;
/* ... remaining dark mode variables */
}
}
Dark Mode Toggle
// components/theme-toggle.tsx
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Form Integration with React Hook Form and Zod
"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 profileSchema = z.object({
username: z.string().min(2).max(30),
email: z.string().email(),
role: z.enum(["admin", "user", "editor"]),
});
type ProfileValues = z.infer<typeof profileSchema>;
export function ProfileForm() {
const form = useForm<ProfileValues>({
resolver: zodResolver(profileSchema),
defaultValues: { username: "", email: "", role: "user" },
});
function onSubmit(data: ProfileValues) {
console.log(data);
}
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>Your public display name.</FormDescription>
<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="editor">Editor</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save Profile</Button>
</form>
</Form>
);
}
Data Table with TanStack Table
"use client";
import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
interface Payment {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
}
const columns: ColumnDef<Payment>[] = [
{ accessorKey: "email", header: "Email" },
{ accessorKey: "status", header: "Status" },
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
return <div className="text-right font-medium">{formatted}</div>;
},
},
];
export function DataTable({ data }: { data: Payment[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<div>
<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.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>
<div className="flex items-center justify-end space-x-2 py-4">
<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>
);
}
Command Palette (cmdk)
"use client";
import { useEffect, useState } from "react";
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command";
import { Calculator, Calendar, CreditCard, Settings, User } from "lucide-react";
export function CommandPalette() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => !prev);
}
};
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><Calendar className="mr-2 h-4 w-4" /> Calendar</CommandItem>
<CommandItem><Calculator className="mr-2 h-4 w-4" /> Calculator</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem><User className="mr-2 h-4 w-4" /> Profile</CommandItem>
<CommandItem><CreditCard className="mr-2 h-4 w-4" /> Billing</CommandItem>
<CommandItem><Settings className="mr-2 h-4 w-4" /> Settings</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
Best Practices
- Use the CLI to add components rather than copying code manually. The CLI resolves dependencies and sets up imports correctly.
- Customize at the CSS variable level for global theme changes. Edit individual component files for structural changes.
- Compose components by combining primitives. A card with a form inside uses Card, Form, Input, Button together.
- Keep component files in
ui/and build feature components that import fromui/. Never modifyui/components for feature-specific logic; wrap them instead. - Use
asChildprop to merge shadcn component behavior onto your own elements, avoiding extra DOM nodes. - Leverage
cva(class-variance-authority) for creating component variants that are consistent with the design system. - Update components by re-running the CLI add command. Diff the changes against your customizations.
Anti-Patterns
- Installing shadcn/ui as an npm package -- it is not meant to be consumed as a dependency. You copy the source code into your project.
- Overriding styles with
!important-- use thecn()utility to merge Tailwind classes properly. The last class wins via tailwind-merge. - Ignoring the Form component and wiring up react-hook-form manually. The Form component provides accessible error handling and label association out of the box.
- Hardcoding colors instead of using CSS variables -- this breaks theming and dark mode. Always reference semantic tokens like
bg-primaryortext-muted-foreground. - Creating deeply nested component wrappers instead of composing at the usage site. shadcn components are designed for flat, explicit composition.
- Skipping accessibility by removing Radix primitives and replacing them with plain divs. The Radix layer provides keyboard navigation, focus management, and screen reader support.
Install this skill directly: skilldb add ui-components-services-skills
Related Skills
Ark UI
"Ark UI: state machine-driven components, headless primitives, Select, Combobox, DatePicker, Toast, styling with any CSS framework"
Daisyui
"daisyUI: Tailwind CSS component library, themes, responsive utilities, component classes, customization, semantic color names"
Headless UI
"Headless UI: unstyled accessible components for Tailwind, Dialog, Combobox, Listbox, Menu, Transition, render props, React/Vue"
Kobalte
Kobalte: accessible, unstyled UI component library for SolidJS with WAI-ARIA compliance and composable compound component APIs
Melt UI
Melt UI: headless, accessible UI component builders for Svelte using the builder pattern with full styling freedom
Park UI
Park UI: pre-designed styled components built on Ark UI primitives, available for React, Vue, and Solid with Panda CSS or Tailwind CSS styling