shadcn/ui Setup
Install, configure, and theme shadcn/ui — copy-paste components built on Radix UI and Tailwind CSS.
Recipe
Quick-reference recipe card — copy-paste ready.
# Initialize shadcn in your project
npx shadcn@latest init
# Add specific components
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add card
# Add multiple at once
npx shadcn@latest add button input card dialog form// components.json — generated by init
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}// lib/utils.ts — the cn utility
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}When to reach for this: At the start of any Next.js or React project where you want polished, accessible UI components with full control over the source code.
Working Example
/* app/globals.css — shadcn theme with CSS variables */
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
}
}// app/layout.tsx
import { ThemeProvider } from "next-themes";
import "@/app/globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="bg-background text-foreground antialiased">
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}// Quick demo using shadcn components
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function LoginCard() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Enter your credentials to continue.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
<Button className="w-full">Sign in</Button>
</CardContent>
</Card>
);
}What this demonstrates:
- shadcn initialization and component installation
- CSS variable-based theming with light and dark mode
ThemeProvidersetup fromnext-themes- Component composition (Card + Input + Button + Label)
- The
cn()utility for merging class names
Deep Dive
How It Works
- shadcn/ui is NOT a package — it copies component source code into your project
- Components live in
components/ui/and are fully editable - Each component uses Radix UI primitives for accessibility and behavior
- Styling uses Tailwind CSS utilities + CSS variables for theming
- The
cn()utility merges class names withclsxand resolves Tailwind conflicts withtailwind-merge - CSS variables enable runtime theme switching without rebuilding
Variations
Custom theme colors:
/* Use the shadcn theme generator at ui.shadcn.com/themes */
:root {
--primary: oklch(0.5 0.2 260); /* custom blue */
--destructive: oklch(0.5 0.2 25); /* custom red */
}Adding a component with dependencies:
# Some components auto-install their dependencies
npx shadcn@latest add dialog
# This installs @radix-ui/react-dialog and the Dialog componentPath aliases setup:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}TypeScript Notes
// cn utility is fully typed
import { cn } from "@/lib/utils";
cn("bg-red-500", undefined, false, "text-white");
// returns "bg-red-500 text-white"
// Component props extend Radix types
// e.g., Button extends React.ButtonHTMLAttributes<HTMLButtonElement>
import { Button, type ButtonProps } from "@/components/ui/button";
function MyButton(props: ButtonProps) {
return <Button variant="outline" {...props} />;
}Gotchas
-
Not a package — it is source code —
npm install shadcnis wrong. Fix: Usenpx shadcn@latest add <component>to copy components into your project. -
Tailwind v4 compatibility — shadcn/ui supports Tailwind v4. Make sure to run the latest
npx shadcn@latest initwhich generates the v4-compatible CSS. -
CSS variables must be defined — If you skip the CSS variable setup, components render with no colors. Fix: Ensure your
globals.cssincludes the full:rootand.darkvariable blocks. -
cn()import path — Components import from@/lib/utils. If your alias differs, updatecomponents.jsonand run init again. -
Component updates — Since components are copied, they do not auto-update. Fix: Re-run
npx shadcn@latest add <component>to get the latest version (but this overwrites your changes).
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Radix UI (bare) | You want primitives without opinions on styling | You want pre-styled, ready-to-use components |
| Headless UI | You use Tailwind but prefer a different primitive library | You want the full shadcn ecosystem |
| MUI / Chakra UI | You want a fully packaged component library | You want full control over component source code |
| Mantine | You want a batteries-included React component library | You use Tailwind for styling |
FAQs
Is shadcn/ui an npm package you install?
No. shadcn/ui copies component source code into your project. Use npx shadcn@latest add <component>, not npm install shadcn.
What does npx shadcn@latest init do?
- Generates
components.jsonwith project settings - Creates
lib/utils.tswith thecn()utility - Sets up CSS variables in
globals.cssfor theming
What is the cn() utility and why is it needed?
cn() combines clsx (conditional class merging) with tailwind-merge (Tailwind conflict resolution). It ensures className overrides work correctly on shadcn components.
Where do shadcn components live in the project?
In components/ui/ by default. Each component is a fully editable source file, not a node_modules dependency.
How do you customize the shadcn theme colors?
Edit the CSS variables in globals.css under :root (light) and .dark (dark). Use the theme generator at ui.shadcn.com/themes for quick presets.
How does dark mode work with shadcn?
CSS variables are defined twice -- once in :root for light mode and once in .dark for dark mode. Use next-themes with attribute="class" to toggle the .dark class.
How do you add multiple components at once?
npx shadcn@latest add button input card dialog formGotcha: Components render with no colors after setup. What is wrong?
The CSS variables are missing. Ensure globals.css includes the full :root and .dark variable blocks generated by npx shadcn@latest init.
Gotcha: You updated a component but your changes were lost. What happened?
Re-running npx shadcn@latest add <component> overwrites the file. Back up your customizations before updating, or use version control.
How is cn() typed in TypeScript?
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}ClassValue accepts strings, arrays, objects, undefined, null, and booleans.
How do you use shadcn component types in TypeScript?
import { Button, type ButtonProps } from "@/components/ui/button";
function MyButton(props: ButtonProps) {
return <Button variant="outline" {...props} />;
}Component props extend their underlying Radix or HTML element types.
Related
- Button — button variants and usage
- Dialog — modal dialogs
- Form — form components with RHF + Zod
- Tailwind Setup — Tailwind v4 configuration