shadcn-ui
shadcn/ui - Component Library Guide
shadcn/ui is a collection of re-usable components built using Radix UI and Tailwind CSS. You can copy and paste the code into your project or use the CLI to add components automatically.
Key Concepts
It's NOT a traditional component library - you own the code. Components are copied into your project, giving you full control to modify them as needed.
Built on:
- Radix UI (accessible primitives)
- Tailwind CSS (styling)
- TypeScript (type safety)
Supported frameworks: Next.js, Vite, Remix, Astro, Laravel, Gatsby, React Router, TanStack Router/Start
Installation
Detect the project type first
Check which framework and package manager the project uses:
# Check framework
ls next.config.* # Next.js
ls vite.config.* # Vite
ls remix.config.* # Remix
# Check package manager
ls package-lock.json # npm
ls yarn.lock # yarn
ls pnpm-lock.yaml # pnpm
Step 1: Install dependencies
For Next.js:
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
For Vite/React:
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
Step 2: Configure Tailwind
Create or update tailwind.config.js:
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config
Step 3: Create utilities file
Create src/lib/utils.ts (or lib/utils.ts):
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Step 4: Add CSS variables
Create or update app/globals.css (Next.js App Router) or src/index.css (Vite):
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
Using the CLI
Initialize shadcn/ui
npx shadcn@latest init
This command:
- Creates
components.jsonconfiguration - Sets up the project structure
- Configures paths and aliases
Add components
# Add a single component
npx shadcn@latest add button
# Add multiple components
npx shadcn@latest add button card input label
# Add with --yes flag to skip prompts
npx shadcn@latest add button --yes
# Add to specific path
npx shadcn@latest add button --path src/components
Common components to add first:
button- Essential for any UIcard- Container for contentinput- Form inputslabel- Form labelsform- Form handling with React Hook Form
Popular Components
Button
import { Button } from "@/components/ui/button"
export default function Page() {
return (
<div>
<Button>Click me</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
</div>
)
}
Card
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
export default function Page() {
return (
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
)
}
Form (with React Hook Form)
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
})
export default function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Dialog (Modal)
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Table
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export default function TableDemo() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
)
}
Dark Mode Setup
Using next-themes (Recommended for Next.js)
npm install next-themes
Create components/theme-provider.tsx:
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
Update app/layout.tsx:
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Add theme toggle:
npx shadcn@latest add theme-toggle
import { ThemeToggle } from "@/components/ui/theme-toggle"
export default function Navbar() {
return (
<nav>
<ThemeToggle />
</nav>
)
}
Customization
Modify colors
Edit the CSS variables in your globals.css:
:root {
--primary: 220 90% 56%; /* Custom blue */
--secondary: 160 60% 45%; /* Custom green */
/* ... */
}
Customize component styles
Each component is in components/ui/. You can modify them directly:
// components/ui/button.tsx
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none",
{
variants: {
// Add your custom variant
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
mycustom: "bg-purple-500 text-white hover:bg-purple-600",
// ...
},
},
}
)
Component Categories
Form & Input
- Button, Input, Textarea, Checkbox, Radio Group, Select, Switch, Slider, Calendar, Combobox, Label, Field, Form
Layout & Navigation
- Accordion, Breadcrumb, Navigation Menu, Sidebar, Tabs, Separator, Scroll Area, Resizable
Overlays & Dialogs
- Dialog, Alert Dialog, Sheet, Drawer, Popover, Tooltip, Hover Card, Context Menu, Dropdown Menu, Menubar, Command
Feedback & Status
- Alert, Toast, Progress, Spinner, Skeleton, Badge, Empty
Display & Media
- Avatar, Card, Table, Data Table, Chart, Carousel, Typography, Item, Kbd
Common Workflows
Creating a form
- Add
form,input,label,buttoncomponents - Install React Hook Form and Zod
- Create schema with Zod
- Build form with Form components
- Handle submission
Building a dashboard
- Add
card,button,tablecomponents - Create layout with
sidebarortabs - Add data visualization with
chart - Include
progressfor loading states
Creating a modal dialog
- Add
dialogcomponent - Use DialogTrigger for button
- Add DialogContent with title/description
- Handle confirmation in DialogFooter
Adding dark mode
- Install next-themes
- Create ThemeProvider wrapper
- Add theme-toggle component
- Test both light/dark modes
Best Practices
- Always add components via CLI -
npx shadcn@latest add [component] - Customize after adding - Components are yours to modify
- Use the cn() utility - Merge Tailwind classes properly
- Check dependencies - Some components need extra packages (React Hook Form, Zod, etc.)
- Read component docs - Visit https://ui.shadcn.com/docs/components/[name] for examples
Troubleshooting
Styles not working:
- Check Tailwind CSS is configured correctly
- Verify content paths in tailwind.config.js
- Ensure CSS variables are defined
Components not found:
- Make sure you ran
npx shadcn@latest add [component] - Check the component path in your import
TypeScript errors:
- Ensure all dependencies are installed
- Check tsconfig.json path aliases (@/components)
Dark mode not working:
- Verify ThemeProvider wraps your app
- Check CSS variables have .dark class
- Ensure suppressHydrationWarning on html tag