- Published on
What is shadcn/ui? Understanding Its Purpose, Benefits, and How to Use It in Modern React Apps
- Authors
What is shadcn/ui? Understanding Its Purpose, Benefits, and How to Use It in Modern React Apps
Modern React projects often face a tough balance: should you use a prebuilt UI library (fast but inflexible) or design your own system (flexible but slow to build)?
That’s where shadcn/ui comes in, a new approach to UI development that gives you full control over your code while offering the polish of a mature component library.
In this guide, we’ll explore what shadcn/ui is, why it’s become so popular among React and Next.js developers, and how you can use it to speed up your own UI work without losing flexibility.
What Is shadcn/ui?
shadcn/ui (often written simply as shadcn) is not a traditional npm package or a themeable component library like Material UI or Chakra UI.
Instead, it’s a collection of high-quality, open-source React components built with:
- Tailwind CSS (for styling)
- Radix UI (for accessible, unstyled primitives)
- Lucide Icons (for clean, consistent SVG icons)
But here’s the twist:
Instead of installing it as a dependency, you copy the actual source code of each component into your own project.
That means every button, dialog, or dropdown lives in your own codebase, editable, themeable, and version-controlled by you.
You can think of it as a component generator or UI starter kit, not a dependency.
How It Works
The shadcn/ui project provides a CLI tool that scaffolds components directly into your project:
npx shadcn-ui@latest add button
This command downloads a prebuilt, accessible Button component and adds it under your local components/ui/ folder. From there, you can customize it, change Tailwind classes, tweak variants, or adapt it to your design system.
Every component is just React + Tailwind + Radix, no hidden logic or runtime dependency.
Why Developers Use shadcn/ui
1. Full Ownership of Code
Unlike UI libraries where you rely on opaque dependencies, shadcn gives you the full component source. You can:
- Edit styles directly
- Add props or variants
- Integrate custom logic (like analytics or permissions)
It’s your UI, your rules, no waiting for maintainers to merge a PR.
2. Built on Proven Foundations
Each component is built using:
- Tailwind CSS → utility-first styling
- Radix UI → accessible and unstyled base primitives
- Lucide Icons → modern SVG icons
- clsx / tailwind-variants → clean conditional class management
This ensures your components are both accessible and production-ready, following design system best practices out of the box.
3. Consistency and Speed
You get a cohesive, beautiful starting point for your UI:
- Consistent spacing, typography, and color system
- Dark mode and theming via CSS variables
- Ready-to-use layout and interactive patterns (dropdowns, modals, tooltips)
You can start shipping UIs that feel designed, without reinventing every component.
4. No Vendor Lock-In
Because the components are just code, you can:
- Fork them
- Replace them
- Mix with other libraries
If shadcn/ui ever changes or you outgrow it, your components still belong entirely to you.
Common Components and Examples
Here are some of the most popular shadcn/ui components and what they look like in action.
Example 1: Button
import { Button } from '@/components/ui/button'
export default function Example() {
return (
<div className="space-x-2">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive">Delete</Button>
</div>
)
}
Why it’s great:
- Built-in variants like
outline,destructive, andghost. - Fully themeable with Tailwind’s design tokens.
- Simple enough to customize without losing consistency.
Understanding the Button Variants
When you run:
npx shadcn-ui@latest add button
The CLI creates a button.tsx file inside components/ui/, typically like this:
// components/ui/button.tsx
import * as React from 'react'
import { cva } 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',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size, className }))} {...props} />
}
export { Button, buttonVariants }
So What Are outline, destructive, and ghost?
They’re style variants pre-defined in that buttonVariants configuration. You don’t need to define them manually, they come with the generated component by default.
Each variant maps to a different Tailwind class pattern:
| Variant | Visual Style | Typical Use |
|---|---|---|
default | Solid primary button (bg-primary, white text) | Main call to action (e.g., “Save”, “Submit”). |
outline | Transparent background, border visible | Secondary action or less emphasis (“Cancel”). |
destructive | Red/danger color (bg-destructive) | Actions with destructive consequences (“Delete”, “Remove”). |
ghost | Minimal styling, subtle hover | Low emphasis buttons (e.g., icons, secondary links). |
link | Styled like a text link | For inline navigation inside text. |
So, when you write:
<Button variant="destructive">Delete</Button>
you’re telling the Button component to use the red danger style, because the variant maps to bg-destructive and text-destructive-foreground.
What Are “Tailwind Design Tokens”?
“Design tokens” are named color and spacing variables used throughout your theme. In shadcn/ui, these are defined in your globals.css (or tailwind.config.js) like so:
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* etc. */
}
Then Tailwind references them as “tokens” using its theme() syntax or class shortcuts:
className = 'bg-primary text-primary-foreground hover:bg-primary/90'
These tokens:
- Keep colors consistent across components.
- Support dark mode automatically via CSS variables.
- Let designers and developers share one visual language (e.g., “primary”, “accent”, “destructive”) instead of raw hex codes.
Why This Is Useful
- You can add or modify variants (e.g.,
success,warning) easily in the samebutton.tsx. - Your UI stays visually consistent since all variants pull from shared design tokens.
- Updating your theme (colors, spacing, etc.) instantly updates all components.
- It’s flexible, use the built-in variants or extend them to match your brand.
In short: Those variants (outline, destructive, ghost, etc.) are prebuilt, design-token-powered styles you can extend or override. They’re part of what makes shadcn/ui components production-ready out of the box, consistent, theme-aware, and easily customizable.
Example 2: Dialog (Modal)
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
export default function Example() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
</DialogHeader>
<p>This action cannot be undone.</p>
</DialogContent>
</Dialog>
)
}
Why it’s great: The dialog uses Radix primitives under the hood, meaning it’s keyboard-accessible, screen-reader-friendly, and responsive out of the box.
Example 3: Input and Form Components
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
export default function LoginForm() {
return (
<form className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
<Button type="submit">Login</Button>
</form>
)
}
Clean, minimal, and easy to extend for any authentication or settings page.
When (and When Not) to Use shadcn/ui
✅ Use It When
- You’re building a custom product (SaaS, dashboard, app) and want consistent UI fast.
- You need full control over design and accessibility.
- You’re already using Tailwind CSS and Next.js.
❌ Avoid It When
- You prefer a drop-in design system (like MUI, Chakra UI).
- You don’t use Tailwind (it’s tightly coupled).
- You want to move extremely fast without touching component code.
Getting Started (Quick Setup)
Install Tailwind CSS (if not already):
npx create-next-app my-app cd my-app npx tailwindcss init -pInitialize shadcn/ui
npx shadcn-ui@latest initAdd components
npx shadcn-ui@latest add button card dialog inputImport and customize Your new components will appear in
components/ui/.
🧩 shadcn vs Other UI Libraries
| Feature | shadcn/ui | Chakra UI | Material UI | Headless UI |
|---|---|---|---|---|
| Code ownership | ✅ You own it | ❌ Library controlled | ❌ Library controlled | ✅ You own it |
| Styling | Tailwind | Styled System | Emotion / CSS | Tailwind |
| Accessibility | ✅ Radix UI | ✅ | ✅ | ✅ |
| Theming | ✅ (CSS variables + Tailwind) | ✅ | ✅ | ⚙️ Custom |
| Learning curve | 🟢 Low | 🟢 Medium | 🔴 High | 🟡 Medium |
| Customization freedom | 🟢 Full | 🟡 Partial | 🔴 Limited | 🟢 Full |
Key Takeaways
- shadcn/ui isn’t a package, it’s a copy-and-own component model.
- You get real control, no lock-in, and great developer experience.
- Built with Tailwind, Radix, and modern React patterns, it’s ideal for teams who want a clean, accessible, and extendable design foundation.
It’s the perfect middle ground between “build everything yourself” and “depend on a heavy UI library.”