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. You own every component. No version conflicts, no black-box styling, no fighting with library defaults. Just open, editable components in your codebase.
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 init
You'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 input
Each 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 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>
);
}
// 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