shadcn/ui

Status: 🟩 COMPLETE (🟦 LIVING) Last updated: 2026-06-20 Plain-English tagline: A library of beautiful React components you copy into your project (rather than install). Built on Tailwind + Radix. The default UI kit for new Next.js projects in 2026.


In plain English

Traditional component libraries (Material UI, Chakra, Mantine) are installed as npm packages: npm install @mui/material, then import components from the package. Your project depends on the library; theming and customization happen through the library’s APIs.

shadcn/ui does something different: you run a CLI to copy the component’s source code into your project. There’s no @shadcn/ui package in your node_modules. The component lives at components/ui/button.tsx, and it’s yours to edit directly.

Why this matters:

  • Total customization — change anything, no fighting the library’s API
  • No version lock-in — the code is in your repo
  • No bundle bloat — only the components you actually use
  • Updates happen on YOUR schedule — pull new versions via the CLI when ready

shadcn/ui components are built on:

  • Tailwind CSS for styling
  • Radix UI for behavior (keyboard navigation, focus management, accessibility)
  • TypeScript throughout
  • CVA (class-variance-authority) for variant management

The result: accessible, beautiful, customizable components. Created by Shadcn (a developer pseudonym) in 2023. Took over rapidly. Now the default for new React/Next.js projects.


Why it matters

For non-coders building real webapps with Claude:

  • Looks professional. Components have great defaults out of the box. No “this looks like a 2010 admin panel” feeling.
  • Accessible. Built on Radix UI, which handles keyboard nav, ARIA, focus rings automatically.
  • Tailwind throughout. Customization is just editing Tailwind classes — no learning a separate theming API.
  • Plays well with Claude. Claude knows shadcn/ui patterns well. “Add a dialog using shadcn” produces clean code.
  • Composable. Components are designed to be composed and customized, not used as black boxes.

For your stack (Next.js + Tailwind), shadcn/ui is essentially the natural extension.


How it works

1. Initial setup

npx shadcn@latest init

Prompts:

  • Style (Default or New York) — both fine; pick one and stick with it
  • Base color (slate, gray, zinc, neutral, stone)
  • CSS variables (yes — enables theming)

The CLI creates:

  • components/ui/ — where copied components will land
  • lib/utils.ts — helper functions (cn() for merging Tailwind classes)
  • components.json — shadcn config
  • Updates tailwind.config.ts to add shadcn’s theme tokens

2. Add components on demand

npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu

Each command copies that component’s source code into components/ui/. You can see, edit, and customize the actual files.

3. Use them

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 
export function MyPage() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Hello</CardTitle>
      </CardHeader>
      <CardContent>
        <Button>Click me</Button>
        <Button variant="outline">Outlined</Button>
        <Button variant="destructive" size="sm">Delete</Button>
      </CardContent>
    </Card>
  );
}

Variants (primary, secondary, outline, destructive, ghost) and sizes (sm, default, lg, icon) are built in. Customize by editing components/ui/button.tsx directly.


The major components available

A non-exhaustive list of what you can add:

Forms: Button, Input, Textarea, Label, Checkbox, Radio Group, Select, Switch, Slider, Toggle, Form (RHF + Zod integration)

Layout: Card, Separator, Tabs, Accordion, Collapsible, Aspect Ratio, Scroll Area, Resizable

Overlays: Dialog, Alert Dialog, Sheet, Drawer, Popover, Hover Card, Tooltip, Context Menu, Dropdown Menu, Menubar, Command (cmd-k)

Feedback: Alert, Toast (Sonner integration), Skeleton, Progress, Spinner, Badge

Data display: Table, Data Table (TanStack Table integration), Avatar, Calendar, Date Picker

Navigation: Navigation Menu, Breadcrumb, Pagination

Other: Theme provider, Toggle group, Carousel, Chart (Recharts integration), Sidebar, Resizable panels

Roughly 50+ components, with new ones added regularly.


A concrete example: a dialog with a form

import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
 
export function CreatePostDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>New post</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Create a new post</DialogTitle>
        </DialogHeader>
        <form className="space-y-4">
          <div>
            <Label htmlFor="title">Title</Label>
            <Input id="title" name="title" />
          </div>
          <div>
            <Label htmlFor="body">Body</Label>
            <Input id="body" name="body" />
          </div>
          <Button type="submit">Publish</Button>
        </form>
      </DialogContent>
    </Dialog>
  );
}

Out of the box:

  • The dialog traps focus when open
  • Escape closes it
  • Clicking outside closes it
  • The trigger gets aria-expanded/aria-controls
  • Smooth open/close animations
  • Mobile-responsive

You write 20 lines of JSX; you get a production-quality modal. The accessibility is from Radix UI under the hood.


Customizing components

Since the source is in your repo, you can edit anything. To change the button’s default variant:

// components/ui/button.tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center ...",  // base
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        // ← change this color/treatment freely
      },
      // ...
    },
  }
);

Edit, save, the change applies everywhere the button is used.

For theme-wide changes (colors, radius, fonts), modify CSS variables in app/globals.css:

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;  /* ← change brand color */
    --radius: 0.5rem;              /* ← change all border-radius */
  }
}

Components reference these tokens (bg-primary, rounded-md), so theme changes ripple everywhere.


Themes

shadcn/ui has a theme picker on its website: change brand color + style + radius, copy the resulting CSS variables, paste into your globals.css. Instant rebrand.

For dark mode, the dark variant uses different CSS variable values, switched by the .dark class on <html>. Pairs naturally with next-themes (see How-to: Set up dark mode).


The mental model

shadcn/ui isn’t a library you install — it’s a distribution system for component recipes. Think of it as “really high-quality component templates, given to you in your own code.”

Implications:

  • Updates are opt-in. If shadcn improves a component, you can pull the new version with the CLI (and merge any local changes manually).
  • There’s no upgrade pain. You’re never blocked by “the library doesn’t support X yet.”
  • You own the bug surface too. If a component has a bug, you fix it in your code.

Common gotchas

  • asChild confuses people. When a shadcn component takes asChild, it doesn’t render its own element — it adopts its child element’s role. <DialogTrigger asChild><Button>...</Button></DialogTrigger> makes the Button itself the trigger, instead of wrapping it in another element. Read the Radix docs when confused.

  • Updating components. npx shadcn@latest add button overwrites your customizations if you say yes. The CLI usually warns and asks. Diff before overwriting.

  • Customization scattered. Once you’ve customized many components, updates become harder. Discipline: keep customizations focused; document them.

  • Tailwind class merging. If you pass className to a shadcn component, the cn() utility merges with defaults — last wins. If you don’t see your override, check class specificity / order.

  • next/link and <a> tags inside. Some shadcn components render their own anchors. Use asChild to compose with <Link> instead of double-anchoring.

  • Theme conflicts. If you change --primary etc. but the components don’t update, you may have multiple CSS variable definitions clashing. Globals.css is the canonical home.

  • Forms with shadcn’s Form component require react-hook-form + Zod. It’s well-designed but the learning curve is real. For simple forms, plain inputs are fine.

  • Charting (Recharts wrapper). The shadcn chart wrapper is a thin layer over Recharts. For complex charts, you may need to drop down to Recharts directly.

  • Mixing with other component libraries. Possible but messy. shadcn + your own custom components is the cleanest path. Mixing shadcn + MUI is asking for trouble.

  • Skipping the initial init. Components reference utilities (cn(), theme variables) that the init creates. Don’t add a component without init.

  • Server Components vs Client Components. Many shadcn components use state, so they’re Client Components. Pages that consume them can still be Server Components; the boundary happens at the component import.


See also

Sources