Modern web development demands both speed and flexibility when building polished user interfaces. Traditional React UI libraries like Material-UI, Ant Design, and Chakra UI offer convenience but often force developers to compromise on control, customizability, and bundle size.
Shadcn UI introduces a new approach: instead of installing a bulky dependency, you copy well-crafted, accessible components directly into your project. This empowers your team to tailor every detail while benefiting from a robust foundation.
In this comprehensive guide, you'll learn how to:
- Understand Shadcn UI's philosophy and how it differs from classic libraries
- Set up Shadcn UI in a Next.js or Vite project
- Build production-ready UIs with real-world components and forms
- Master theming, dark mode, and advanced customization
- Apply best practices for scalable, maintainable frontends
💡 Looking for an API platform that helps you deliver as seamlessly as Shadcn UI? Generate beautiful API documentation and boost your team's productivity with integrated collaboration tools. Apidog is your all-in-one solution—replace Postman for a better price!
Why Shadcn UI? Solving Real Developer Pain Points
Before diving into code, it's vital to grasp why Shadcn UI exists:
Shadcn UI Is Not:
- An npm library: You won't install it as a package; you copy source files into your
components/ui/directory. - A monolith: Only include the components you actually use.
- Restrictive: The code is yours to change—no more fighting CSS overrides or limited themes.
What Makes Shadcn UI Unique
- Own the Source: Every component is a
.tsxfile in your codebase. Edit structure, style, and logic freely. - Built on the Best Tools:
- Tailwind CSS: Utility-first styling for rapid, precise UI development.
- Radix UI: Unstyled, accessible primitives for robust keyboard and screen reader support.
- Hybrid Approach: Get the speed of prebuilt components without sacrificing long-term flexibility.
This model is ideal for API-driven teams who value code ownership, custom branding, and accessibility—while maintaining rapid development velocity.
Getting Started: Project Setup with Shadcn UI
Let's set up a new project using Next.js (recommended for API-focused apps). We'll also briefly cover Vite for those using React without Next.js.
Prerequisites
- Node.js (latest LTS)
- npm (or yarn/pnpm)
1. Create Your Next.js Project
In your terminal, run:
npx create-next-app@latest my-shadcn-app --typescript --tailwind --eslint
Recommended setup choices:
- Use
src/directory? Yes - Use App Router? Yes
- Customize import alias? No
Navigate to your app:
cd my-shadcn-app
2. Initialize Shadcn UI
From your project root, run:
npx shadcn-ui@latest init
You'll answer a series of prompts:
- Use TypeScript? Yes
- Style: Default (or New York)
- Base color: Slate (customizable later)
global.csslocation: auto-detected (src/app/globals.css)- Use CSS variables for theming? Yes
tailwind.config.tslocation: auto-detected- Component alias:
@/components - Utils alias:
@/lib/utils - Using React Server Components? Yes
- Write config to
components.json? Yes
The CLI will:
- Install dependencies (
tailwindcss-animate,class-variance-authority) - Update Tailwind and global styles
- Set up a
cnclass utility
Your project is now Shadcn-ready!
Vite Setup?
Follow a similar process—after scaffolding your Vite + React + TS app, install Tailwind, then run npx shadcn-ui@latest init. The CLI will guide you based on your file structure.
Building with Shadcn UI: From Simple to Advanced
1. Add Your First Components
Clean up the Next.js boilerplate. Start with a Button:
npx shadcn-ui@latest add button
This creates src/components/ui/button.tsx.
Add a Card:
npx shadcn-ui@latest add card
Now, in src/app/page.tsx, use these components:
import { Button } from "@/components/ui/button";
import {
Card, CardHeader, CardTitle, CardDescription,
CardContent, CardFooter
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; // Add next
import { Label } from "@/components/ui/label"; // Add next
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-background p-8">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Create Project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="grid gap-2">
<Label htmlFor="framework">Framework</Label>
<Input id="framework" placeholder="e.g. Next.js" />
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Deploy</Button>
</CardFooter>
</Card>
</main>
);
}
If you haven't yet:
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npm run dev
Check your app at http://localhost:3000.
2. Add Interactive Components
Replace the static "Framework" input with a Select dropdown:
npx shadcn-ui@latest add select
The CLI ensures dependencies like Popover are present.
Update your form:
import {
Select, SelectTrigger, SelectValue, SelectContent, SelectItem
} from "@/components/ui/select";
// ...within CardContent
<div className="grid gap-2">
<Label htmlFor="framework">Framework</Label>
<Select>
<SelectTrigger id="framework">
<SelectValue placeholder="Select a framework" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nextjs">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
</div>
Add Toast Notifications
Want feedback when "Deploy" is clicked?
npx shadcn-ui@latest add toast
In src/app/layout.tsx:
import { Toaster } from "@/components/ui/toaster";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
);
}
In your page, enable client-side code and add a toast on button click:
'use client';
import { useToast } from "@/components/ui/use-toast";
export default function Home() {
const { toast } = useToast();
function handleDeploy() {
toast({
title: "Deployment Scheduled!",
description: "Your project is being deployed.",
duration: 5000,
});
}
// ... return JSX ...
<Button className="w-full" onClick={handleDeploy}>Deploy</Button>
}
Building Forms with Validation (react-hook-form + zod)
Complex apps need robust, type-safe forms. Shadcn UI pairs seamlessly with react-hook-form and zod.
1. Install Form Dependencies
npm install react-hook-form zod @hookform/resolvers
2. Add Shadcn Form Component
npx shadcn-ui@latest add form
3. Create a Validation Schema
In your page:
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage
} from "@/components/ui/form";
const formSchema = z.object({
projectName: z.string().min(2, "Project name must be at least 2 characters.").max(50),
framework: z.string({ required_error: "Please select a framework." }),
});
4. Wire Up the Form
export default function Home() {
const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { projectName: "" },
});
function onSubmit(values: z.infer<typeof formSchema>) {
toast({
title: "Submitted Values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
});
}
return (
<main className="flex min-h-screen items-center justify-center bg-background p-8">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Create Project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name of your project" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="framework"
render={({ field }) => (
<FormItem>
<FormLabel>Framework</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a framework" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="nextjs">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
<FormDescription>
The framework you want to deploy.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Deploy</Button>
</form>
</Form>
</CardContent>
</Card>
</main>
);
}
Try submitting with empty fields to see validation in action!
Advanced Customization: Theming and Component Editing
1. Global Theming with CSS Variables
Open src/app/globals.css and locate the :root and .dark blocks. These HSL variables control your entire color palette and border radii.
:root {
--background: 0 0% 100%;
--primary: 222.2 47.4% 11.2%;
--radius: 0.5rem;
/* ... */
}
.dark {
--background: 222.2 84% 4.9%;
--primary: 210 40% 98%;
/* ... */
}
- Change colors: Use HSL values for your brand.
- Adjust border radius: Set
--radiusfor global sharpness or roundness.
2. Enable Dark Mode (Next.js Example)
Install a theme switcher:
npm install next-themes
Create src/components/theme-provider.tsx:
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ children, ...props }) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
Wrap your root layout:
import { ThemeProvider } from "@/components/theme-provider";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
);
}
Add a toggle button (src/components/mode-toggle.tsx):
"use client"
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="outline"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<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>
);
}
Place <ModeToggle /> anywhere in your app for instant dark mode.
3. Editing Component Source: Example (Button Variant)
Need a custom button variant? Edit src/components/ui/button.tsx:
const buttonVariants = cva(
// ...base styles
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
success: "bg-green-600 text-white hover:bg-green-600/90", // New!
// ... other variants
},
// ...size variants
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
Now use: <Button variant="success">Success</Button>
Best Practices for Large-Scale Apps
- Organize Components: Use
components/uifor base Shadcn UI, and separate complex, app-specific composites intocomponents/sharedorcomponents/features. - Stay Updated: To sync with upstream improvements, run
npx shadcn-ui@latest add button—the CLI lets you diff and merge changes. - Accessibility First: Radix primitives ensure default accessibility, but always check color contrast and keyboard navigation when customizing.
Conclusion: Become the Author of Your UI Library
Shadcn UI gives you total control—every component is yours to adapt, extend, and perfect. By leveraging the strengths of Tailwind CSS and Radix UI, it empowers API-focused teams to build scalable, accessible, and uniquely branded UIs at top speed.
For developer teams who value ownership and productivity, this approach is transformative. And when your backend and API tooling needs to keep pace, consider how Apidog supports collaboration and seamless API documentation so your entire stack stays developer-first and efficient. Boost your team's productivity and upgrade your API workflow at a better price.



