Shadcn
Master Shadcn, the revolutionary approach to building component libraries that gives you full control over your components through copy-paste, not NPM packages.
Shadcn
Shadcn is a collection of beautifully designed, accessible components that you can copy and paste into your apps. It's not a traditional component library - it's a new way to build your component library.
What Makes Shadcn Different?
Not a Component Library
Unlike traditional libraries where you install from NPM and import components, Shadcn provides the actual component code that you copy into your project. This means:
Traditional Libraries:
yarn add some-ui-library
import { Button } from 'some-ui-library'
Shadcn:
yarn dlx shadcn@latest add button
- Components are copied to your project
- You own the code
- Full customization control
- No package dependency
Core Principles
1. Open Code
The top layer of your component code is completely open for modification:
- Full Transparency: See exactly how each component is built
- Easy Customization: Modify any part of a component directly
- No Overrides Needed: Edit the source code instead of overriding styles
- AI Integration: LLMs can read, understand, and improve your components
// Traditional library - limited customization
<Button sx={{ /* override styles */ }} />
// Shadcn - edit the source directly
// components/ui/button.tsx
export function Button({ children, ...props }) {
// Modify anything here!
return <button {...props}>{children}</button>
}
2. Composition
Every component shares a common, composable interface:
- Predictable API: Consistent patterns across all components
- Easy Learning: Same approach for every component
- Third-party Friendly: Integrate external components seamlessly
- Team Productivity: Developers and AI understand the patterns
// All components follow similar patterns
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
3. Distribution
Shadcn uses a schema-based distribution system:
- Schema: Flat-file structure defining components and dependencies
- CLI: Command-line tool for distributing and installing components
- Cross-framework: Works across different frameworks
- Custom Distribution: Use the schema to distribute your own components
4. Beautiful Defaults
Components come with carefully chosen default styles:
- Good Out-of-the-Box: Clean, minimal look without configuration
- Unified Design: Components naturally fit together
- Easily Customizable: Simple to override and extend defaults
- Professional: Designed for production applications
5. AI-Ready
Designed to work seamlessly with AI tools:
- Open Code: AI can read and understand component structure
- Consistent API: Predictable patterns for AI to learn
- Generate Components: AI can create new components based on existing patterns
- Suggest Improvements: AI can analyze and optimize your components
Technology Stack
Shadcn is built on top of industry-leading technologies:
Radix UI
- Unstyled, accessible component primitives
- Built-in keyboard navigation
- Screen reader support
- Focus management
- ARIA attributes
Tailwind CSS
- Utility-first styling
- Highly customizable
- Responsive design
- Dark mode support
- Custom theming
TypeScript
- Full type safety
- IntelliSense support
- Better developer experience
- Catch errors early
Installation (Next.js)
Prerequisites
Ensure you have a Next.js project set up:
yarn create next-app my-app --typescript --tailwind --eslint
cd my-app
Initialize Shadcn
Run the initialization command:
yarn dlx shadcn@latest init
You'll be asked to configure your project:
✔ Preflight checks.
✔ Verifying framework. Found Next.js.
✔ Validating Tailwind CSS.
✔ Validating import alias.
✔ Which style would you like to use? › New York
✔ Which color would you like to use as base color? › Zinc
✔ Would you like to use CSS variables for colors? › yes
Configuration Options:
- Style: Choose between "Default" or "New York" design
- Base Color: Select primary color (Slate, Gray, Zinc, Neutral, Stone)
- CSS Variables: Use CSS variables for theming
What Gets Configured
The CLI will:
-
Install dependencies:
{ "dependencies": { "@radix-ui/react-*": "...", "class-variance-authority": "...", "clsx": "...", "tailwind-merge": "..." } } -
Create
components.json:{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "zinc", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } -
Update
tailwind.config.ts -
Add utility functions to
lib/utils.ts -
Configure global CSS with theme variables
Adding Components
Add Individual Components
yarn dlx shadcn@latest add button
This creates:
components/
└── ui/
└── button.tsx
Add Multiple Components
yarn dlx shadcn@latest add button card dialog
View Available Components
yarn dlx shadcn@latest add
You'll see a list of all available components to choose from.
Basic Usage
Button Component
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 size="sm">Small</Button>
<Button size="lg">Large</Button>
</div>
)
}
Card Component
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export default function CardDemo() {
return (
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
)
}
Dialog Component
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
Form Example with React Hook Form
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
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, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
})
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>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Customization
Theming with CSS Variables
Edit app/globals.css to customize colors:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... more variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... more variables */
}
}
Customizing Components
Since you own the code, customize directly:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
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 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
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 hover:bg-accent hover:text-accent-foreground",
// Add your custom variant
brand: "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
// Add your custom size
xl: "h-14 px-10 text-lg rounded-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
Now use your custom variant:
<Button variant="brand" size="xl">Custom Button</Button>
Dark Mode
Shadcn supports dark mode out of the box:
Setup with next-themes
yarn add next-themes
// app/providers.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
// app/layout.tsx
import { ThemeProvider } from "@/components/providers"
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>
)
}
Theme Toggle
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
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>
)
}
Available Components
Shadcn provides 50+ components:
Layout
- Aspect Ratio
- Container
- Separator
Forms
- Button
- Checkbox
- Input
- Label
- Radio Group
- Select
- Slider
- Switch
- Textarea
- Form (with React Hook Form)
Data Display
- Avatar
- Badge
- Card
- Table
- Tabs
Feedback
- Alert
- Alert Dialog
- Dialog
- Progress
- Skeleton
- Toast
Navigation
- Breadcrumb
- Command
- Dropdown Menu
- Menu
- Navigation Menu
- Pagination
Overlays
- Dialog
- Drawer
- Popover
- Sheet
- Tooltip
And Many More
- Calendar
- Carousel
- Collapsible
- Context Menu
- Data Table
- Date Picker
- Scroll Area
- Sonner (Toast)
Building a Dashboard Example
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export default function Dashboard() {
return (
<div className="container mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Dashboard</h1>
<Button>Create New</Button>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Subscriptions
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">
+180.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">
+19% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Now
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">
+201 since last hour
</p>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
)
}
Advantages
Full Code Ownership - You own and control all component code
No Package Lock-in - Not dependent on NPM packages
Easy Customization - Modify components directly without overrides
Accessible - Built on Radix UI with full accessibility support
Beautiful Defaults - Professional design out of the box
TypeScript - Full type safety and IntelliSense
Tailwind CSS - Utility-first styling with easy customization
Dark Mode - Built-in dark mode support
Composable - Consistent API across all components
AI-Ready - Open code structure perfect for AI tools
Active Development - Regular updates and new components
Great Documentation - Comprehensive docs with examples
Disadvantages
More Files - Components are added to your codebase (not a single package)
Manual Updates - Need to manually update components (no automatic package updates)
Initial Setup - Requires Tailwind CSS and configuration
Learning Curve - Need to understand Radix UI and Tailwind CSS
Version Tracking - Harder to track which version of components you're using
When to Use Shadcn
Choose Shadcn When:
Building custom design systems
Need complete control over component code
Want to avoid NPM package dependencies
Working with Tailwind CSS
Building Next.js applications
Need accessible components out of the box
Want AI-friendly component structure
Prefer copy-paste over npm install
Choose Alternatives When:
Need automatic package updates
Don't want to manage component files
Not using Tailwind CSS
Need a complete design system (use Material UI, Ant Design)
Want minimal configuration
Building simple prototypes (use Bootstrap)
Shadcn vs. Traditional Libraries
| Feature | Shadcn | Material UI | Chakra UI | Tailwind CSS |
|---|---|---|---|---|
| Installation | Copy-paste | npm install | npm install | npm install |
| Code Ownership | Full | No | No | Styles only |
| Customization | Easy | Moderate | Easy | Easy |
| Bundle Size | Small | Large | Medium | Small |
| Accessibility | Built-in | Built-in | Built-in | Manual |
| TypeScript | Yes | Yes | Yes | Config |
| Dark Mode | Built-in | Built-in | Built-in | Manual |
| Updates | Manual | Automatic | Automatic | Automatic |
| Components | 50+ | 100+ | 50+ | 0 (utilities) |
| Learning Curve | Moderate | Moderate | Easy | Moderate |
Best Practices
1. Component Organization
components/
├── ui/ # Shadcn components
│ ├── button.tsx
│ ├── card.tsx
│ └── dialog.tsx
├── forms/ # Custom form components
│ └── login-form.tsx
└── layouts/ # Layout components
└── dashboard-layout.tsx
2. Create Compound Components
// components/user-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
export function UserCard({ user }) {
return (
<Card>
<CardHeader>
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
<CardTitle>{user.name}</CardTitle>
</CardHeader>
<CardContent>
<p>{user.email}</p>
<Button className="mt-4">View Profile</Button>
</CardContent>
</Card>
)
}
3. Track Component Versions
Create a COMPONENTS.md file:
# Component Versions
Last Updated: 2024-12-01
- Button: v1.0.0
- Card: v1.0.0
- Dialog: v1.0.0
4. Use Consistent Theming
// lib/theme.ts
export const theme = {
colors: {
primary: 'hsl(221.2 83.2% 53.3%)',
secondary: 'hsl(210 40% 96.1%)',
},
radius: {
sm: '0.25rem',
md: '0.5rem',
lg: '1rem',
},
}
5. Extend Components Carefully
Keep the original component and create variants:
// components/ui/button-loading.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
interface LoadingButtonProps extends ButtonProps {
loading?: boolean
}
export function LoadingButton({ loading, children, ...props }: LoadingButtonProps) {
return (
<Button disabled={loading} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
)
}
Resources
Official Resources
Learning Resources
Community
Related Technologies
- Radix UI - Unstyled component primitives
- Tailwind CSS - Utility-first CSS framework
- CVA (Class Variance Authority) - Variant management
- clsx - Class name utility
Shadcn represents a paradigm shift in how we think about component libraries. Instead of being locked into an NPM package, you get full ownership of beautiful, accessible components that you can customize to your heart's content. It's the perfect choice for teams that want the benefits of a component library without giving up control over their code.