shadcn/ui Reference: Install, Components, Forms, Data Table & Theming
shadcn/ui is not a component library — it’s a collection of copy-paste components built on Radix UI + Tailwind CSS. You own the code. Components live in your src/components/ui/ directory and you modify them directly. This is the key difference from installing a library: no version lock-in, no fighting component APIs, full control. The CLI installs components individually and wires up dependencies automatically.
1. Setup & CLI
Install, init, add components, and understand the copy-paste model
# New project (Next.js):
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npx shadcn@latest init
# init prompts:
# ✓ Which style? → New York (or Default — prefer New York, more refined)
# ✓ Which base color? → Slate (or Zinc/Stone/Gray/Neutral)
# ✓ Use CSS variables? → Yes (recommended — enables theming)
# components.json (created by init):
{
"style": "new-york",
"rsc": true, # React Server Components
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils" # cn() helper lives here
}
}
# Add individual components:
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add table
npx shadcn@latest add data-table # includes @tanstack/react-table
# Components land in src/components/ui/button.tsx — EDIT THEM FREELY
# Update a component to latest shadcn version:
npx shadcn@latest add button --overwrite # use with care — overwrites your edits
2. Core Components — Button, Dialog, Form
Button variants, Dialog with state, Form with react-hook-form + zod validation
// Button — variants and sizes:
import { Button } from "@/components/ui/button";
<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Trash2 className="h-4 w-4" /></Button>
<Button disabled>Loading...</Button>
<Button asChild><Link href="/about">About</Link></Button> {/* render as Link */}
// Dialog:
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
</DialogHeader>
<p>This action cannot be undone.</p>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={() => { onConfirm(); setOpen(false); }}>Delete</Button>
</div>
</DialogContent>
</Dialog>
);
}
3. Form with react-hook-form + zod
shadcn Form component wires react-hook-form and zod together with accessible error messages
// npx shadcn@latest add form input select textarea
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const schema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email"),
});
type FormValues = z.infer<typeof schema>;
export function ProfileForm() {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: "", email: "" },
});
function onSubmit(values: FormValues) {
console.log(values);
}
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 placeholder="Alice Smith" {...field} />
</FormControl>
<FormMessage /> {/* shows zodError message */}
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Saving..." : "Save"}
</Button>
</form>
</Form>
);
}
4. Data Table, Toast & Theming
Data table with @tanstack/react-table, Toaster notifications, and CSS variable theming
// Data Table (npx shadcn@latest add data-table):
import { useReactTable, getCoreRowModel, flexRender,
getSortedRowModel, getPaginationRowModel } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
const columns = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
{ accessorKey: "role", header: () => <div className="text-right">Role</div>,
cell: ({ row }) => <Badge>{row.original.role}</Badge> },
];
function UsersTable({ data }: { data: User[] }) {
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel() });
return (
<Table>
<TableHeader>{table.getHeaderGroups().map(hg => (
<TableRow key={hg.id}>{hg.headers.map(h => (
<TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.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>
);
}
// Toast (npx shadcn@latest add sonner):
import { toast } from "sonner";
toast.success("Saved successfully!");
toast.error("Something went wrong", { description: "Please try again." });
toast.promise(saveData(), { loading: "Saving...", success: "Saved!", error: "Error" });
// CSS variable theming (globals.css) — override to create a custom theme:
// :root { --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; }
// .dark { --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; }
5. cn() Utility, Custom Variants & Server Components
cn() class merging, cva() for variants, and RSC compatibility
// cn() — merge Tailwind classes without conflicts (lib/utils.ts):
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Usage — conditional + merged classes:
<div className={cn("p-4 rounded-lg", isError && "bg-red-50 border-red-200", className)} />
// twMerge handles: cn("p-4", "p-8") → "p-8" (last wins, no duplicate)
// cva() — class variance authority for component variants (already used in Button):
import { cva, type VariantProps } from "class-variance-authority";
const alertVariants = cva("rounded-lg border p-4 flex gap-3", {
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-red-200 bg-red-50 text-red-700",
success: "border-green-200 bg-green-50 text-green-700",
},
},
defaultVariants: { variant: "default" },
});
interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}
const Alert = ({ variant, className, ...props }: AlertProps) =>
<div className={cn(alertVariants({ variant }), className)} {...props} />;
// Server Components — shadcn components are RSC-compatible by default.
// Exceptions: anything with state/hooks (Dialog, DropdownMenu, Toast) — add "use client".
// Pattern: Server Component wraps → passes data → Client Component handles interactions:
// page.tsx (Server): const users = await db.users.findMany()
// UsersClient.tsx (Client): "use client"; function UsersClient({ users }) { ... }
Track Node.js and frontend releases at ReleaseRun. Related: Next.js App Router Reference | Zod Reference | TypeScript Reference
🔍 Free tool: npm Package Health Checker — check shadcn/ui dependencies — Radix UI, Tailwind — for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com