AiTechWorlds
AiTechWorlds
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 CSS, copied directly into your codebase instead of installed as a dependency.
Think of buying furniture unassembled and fully customizable versus a sealed, factory sealed piece you can only paint over. A traditional component library is the sealed piece you fight its theming API to change anything. Shadcn UI hands you the raw parts already cut to size: you own every component, so there's no version conflict, no black box styling, no fighting library defaults.
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.
npx shadcn@latest initYou'll be prompted:
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.
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.
# 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 inputEach command copies the component into src/components/ui/.
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>
);
}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>
);
}Shadcn's Form component works with react hook form and Zod:
npm install react-hook-form @hookform/resolvers zodimport { 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>
);
}// 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>;
}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