TL;DR: shadcn/ui isn't a library you install - you copy components directly into your project. Each component is yours to modify, built on Radix UI primitives and Tailwind CSS. Run
npx shadcn@latest add buttonand it lands incomponents/ui/. Official docs: ui.shadcn.com.
What is shadcn/ui?
shadcn/ui flips the traditional component library model on its head. Instead of installing a package and importing components, you copy the source code directly into your project. Every component lives in your components/ui/ folder - you own it, you can edit it, and there's no upstream API to break your build.
Built on Radix UI primitives for accessibility and Tailwind CSS for styling, it gives you a professional design system baseline in minutes while keeping full control.
When to use it
Use shadcn/ui when you want a polished starting point you can actually modify. It's the right choice for:
- New React/Next.js projects that need a design system fast
- Teams who want full ownership of component code
- Projects already using Tailwind CSS
- Apps where you'll inevitably need to diverge from a library's defaults
Avoid it if your team wants strictly versioned, update-able components - there's no npm update for components you've copied.
Why the copy-paste model matters
The thing that throws people off at first is also the whole point. You're not subscribing to a library; you're forking a snapshot of well-built components into your repo. That trade-off has real consequences once a project grows.
On the plus side, you never hit the classic library wall where a designer asks for a small tweak and the component's props simply don't allow it. With shadcn/ui the source is right there in components/ui/, so you open the file and change it. No wrapper hacks, no !important overrides, no waiting on a maintainer to merge a feature request. I've shipped products where the entire "design system" was just a customized button.tsx and card.tsx, and that was enough.
The cost shows up later. When the upstream project fixes a bug or improves an accessibility detail, you don't get it automatically. You have to re-run the add command for that component, then diff it against your edited version and reconcile by hand. For a small set of components this is a five-minute job. For fifty heavily-edited components, it's a chore nobody wants. My rule of thumb: treat each copied component like code you wrote, because as far as your repo is concerned, you did.
How theming and tokens work
Theming runs entirely through CSS variables defined in your globals.css. The init command writes two sets of tokens, one for light mode and one scoped under the .dark class. Component classes then reference semantic names like bg-background, text-foreground, and border-border instead of hard-coded colors. Want a different brand color? Change one variable and every component follows.
This is genuinely nice for multi-brand or white-label apps. You can swap the whole palette by loading a different set of variable values, no recompile of component code needed. The newer versions lean on OKLCH color values, which keep contrast more consistent across hues than the old HSL approach. If you're supporting a strict contrast requirement, this matters more than it sounds.
One practical tip: keep your custom tokens in the same :root and .dark blocks the CLI generates, rather than scattering them. When you later add a component that expects --popover or --muted-foreground, having all tokens in one place saves a confusing debugging session where a dropdown renders with no background.
Working with Next.js and Server Components
Most shadcn/ui components are happy as server components until they need interactivity. A static Card or Badge ships zero client JavaScript. The moment a component uses a Radix primitive with open/close state, like Dialog, DropdownMenu, or Popover, it needs the "use client" directive at the top of the file. The CLI already adds this where required, so you rarely think about it, but it explains why some files in components/ui/ are client components and others aren't.
A common mistake is wrapping a whole page in a client boundary just to use one interactive button. Don't. Keep the page a server component, import the small client component, and let React's boundaries do their job. You'll keep your bundle lean and your initial render fast.
Key Features
- No runtime dependency - components are your code, not a package
- Accessible by default - every interactive component is Radix UI under the hood (ARIA, keyboard nav, focus management)
- CLI scaffolding -
npx shadcn@latest add buttoncopies the component + its deps - Theme tokens -
globals.cssCSS variables for easy light/dark mode - Registry system - community-built components installable the same way
Installation
npx shadcn@latest init
Then add individual components as needed:
npx shadcn@latest add button dialog select
Basic Usage
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
export function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
TypeScript Tips
shadcn/ui components expose Radix's prop types. Use React.ComponentPropsWithoutRef to forward props cleanly:
import * as React from 'react'
import { Button } from '@/components/ui/button'
// Extending a shadcn button with custom props
interface IconButtonProps extends React.ComponentPropsWithoutRef<typeof Button> {
icon: React.ReactNode
}
export function IconButton({ icon, children, ...props }: IconButtonProps) {
return (
<Button {...props}>
{icon}
{children}
</Button>
)
}
Common Gotchas
asChildprop - use it when you want a component to render as its child instead of its default element (e.g., wrapping a Next.js<Link>in a<Button>)- Dark mode - requires
classstrategy in Tailwind config and a theme toggle that setsclass="dark"on<html> - CSS variable collisions - if your project already uses
--backgroundor--foregroundvariables, rename them before init
Related
- Radix UI - shadcn/ui is built on Radix UI primitives; use Radix directly when you need more composability
- Headless UI - Tailwind-first headless alternative from Tailwind Labs
- Lucide React - the default icon set for shadcn/ui components; tree-shakeable SVG icons that match the minimal aesthetic
- React Resizable Panels - shadcn/ui's ResizablePanelGroup is built on this library; use it directly for custom split-view layouts
- CSS Container Queries Guide - pair container queries with shadcn/ui cards and layouts for truly responsive components