Shadcn UI Component Library
Shadcn UI Component Library
Shadcn UI isn't a traditional npm package — it's a collection of copy-paste components built on Radix UI primitives and styled with Tailwind. You own every component. No version conflicts, no black-box styling, no fighting with library defaults. Just open, editable components in your codebase.
Why Shadcn UI
Traditional component libraries (MUI, Ant Design, Chakra) are installed as npm packages. You customize them through a complex theming API. When the default styles don't match your design, you fight specificity battles.
Shadcn UI flips this: run a CLI command, the component is copied into src/components/ui/. It's your code — edit it directly, style it however you want.
Setup
npx shadcn@latest init
You'll be prompted:
- Which style? → Default (recommended to start)
- Base color? → Slate / Gray / Zinc (your preference)
- CSS variables? → Yes
This sets up tailwind.config.ts with the CSS variable system, adds globals.css with the CSS variables, and creates src/lib/utils.ts with a cn() helper.
The cn() Utility
Shadcn's components use cn() everywhere — it merges class names and handles conditional classes:
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Usage
<div className={cn(
"base-class",
condition && "conditional-class",
variant === "primary" && "bg-blue-600",
className // accept className prop for overrides
)} />
twMerge resolves conflicts — if you pass bg-blue-600 and bg-red-500, it uses the last one instead of both.
Adding Components
# Add individual components
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add toast
npx shadcn@latest add table
npx shadcn@latest add dropdown-menu
# Or add multiple at once
npx shadcn@latest add button card dialog form input
Each command copies the component into src/components/ui/.
Using Shadcn Components
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function CoursePage() {
return (
<Card className="max-w-md">
<CardHeader>
<CardTitle>React Complete Course</CardTitle>
<CardDescription>
Master React from basics to production patterns
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
24 lessons · 8 hours of content · Certificate included
</p>
<div className="flex gap-3">
<Button>Enroll Now</Button>
<Button variant="outline">Preview</Button>
</div>
</CardContent>
</Card>
);
}
Dialog (Modal)
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
function DeleteCourseDialog({ courseId }: { courseId: string }) {
async function handleDelete() {
await deleteCourse(courseId);
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Course</DialogTitle>
<DialogDescription>
This action cannot be undone. The course and all its lessons will be permanently deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive" onClick={handleDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Form with Validation (react-hook-form + zod)
Shadcn's Form component works with react-hook-form and Zod:
npm install react-hook-form @hookform/resolvers zod
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";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const courseSchema = z.object({
title: z.string().min(3, "Title must be at least 3 characters").max(200),
description: z.string().min(10, "Please provide a longer description"),
price: z.coerce.number().min(0, "Price cannot be negative").max(999),
});
type CourseForm = z.infer<typeof courseSchema>;
export function CreateCourseForm() {
const form = useForm<CourseForm>({
resolver: zodResolver(courseSchema),
defaultValues: { title: "", description: "", price: 0 },
});
async function onSubmit(data: CourseForm) {
await createCourse(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Course Title</FormLabel>
<FormControl>
<Input placeholder="React Complete Course 2026" {...field} />
</FormControl>
<FormDescription>
This is the title students will see.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Price (USD)</FormLabel>
<FormControl>
<Input type="number" min="0" step="0.01" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Creating..." : "Create Course"}
</Button>
</form>
</Form>
);
}
Toast Notifications
// Setup in layout.tsx
import { Toaster } from "@/components/ui/toaster";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
);
}
// Use anywhere
"use client";
import { useToast } from "@/components/ui/use-toast";
function SaveButton() {
const { toast } = useToast();
async function handleSave() {
await saveData();
toast({
title: "Saved!",
description: "Your changes have been saved.",
});
}
return <button onClick={handleSave}>Save</button>;
}
Customizing Components
Since components are in your codebase, edit them directly:
// src/components/ui/button.tsx — this is yours to edit
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
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 hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
// Add your own variant:
brand: "bg-brand-500 text-white hover:bg-brand-600",
},
// ...
}
}
);
Next lesson: Dark mode implementation — system-level theming and user preference persistence.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises