Skip to content

Web Development · UI Development

shadcn/ui in 2026: The Component Library That Refuses to Be a Dependency

shadcn/ui is now the default starting point for React UIs. Here's what it actually is, why copy-paste beats npm install, and how to use it without accumulating a mess.

Anurag Verma

Anurag Verma

7 min read

shadcn/ui in 2026: The Component Library That Refuses to Be a Dependency

Sponsored

Share

shadcn/ui is one of the stranger success stories in frontend development. It’s not a component library in the conventional sense. You can’t add it to package.json. There’s no shadcn/ui in node_modules. It’s a CLI that copies source code into your project, and somehow this approach has become the default starting point for React UIs in 2026.

Understanding why requires stepping back from how component libraries have worked for the past decade.

The Problem with Traditional Component Libraries

The standard model: add @mui/material or @chakra-ui/react to your dependencies, import components, apply some theming, and ship. You get 50+ production-ready components out of the box.

The problems show up over time:

You don’t own the components. When you need a <Button> with a loading state that the library didn’t implement the way you want, you either work around the library’s API or fork it. Customization hits walls.

Version coupling. The library’s release cycle is not your release cycle. Major versions bring breaking changes on the library’s timeline, not yours. Every upgrade is a decision.

Bundle size. Most apps use 8-12 of the 50+ components a library ships. The rest rides along.

Style conflicts. MUI and Chakra each have opinions about CSS that conflict with each other and sometimes with your own. Global style resets, specificity wars.

shadcn/ui cuts through this by doing the opposite: it gives you the source code. You run:

npx shadcn@latest add button

A button.tsx file appears in your components/ui/ directory. The file uses Tailwind CSS classes and Radix UI primitives. You can read it. You can modify it. It’s your code now.

The Foundation: Radix UI + Tailwind + class-variance-authority

shadcn/ui is a curated layer on top of existing primitives:

Radix UI handles the hard accessibility work: keyboard navigation, ARIA attributes, focus management, and screen reader announcements. A Radix Dialog component correctly traps focus, restores it on close, handles the Escape key, and announces itself to screen readers. These are the things that are genuinely difficult to implement correctly and almost never worth building from scratch.

Tailwind CSS handles styling. Every component is styled with utility classes, so theming is a matter of changing CSS variables, not overriding stylesheet specificity chains.

class-variance-authority (cva) provides the variant API that makes component customization predictable:

import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50",
  {
    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 bg-background hover:bg-accent",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      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> {
  asChild?: boolean
}

export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
  const Comp = asChild ? Slot : "button"
  return (
    <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />
  )
}

This is the actual button component that gets copied into your project. You can add a loading variant right here. No library API to navigate, no override patterns to learn.

Theming Without the Overhead

The theming system uses CSS custom properties on :root and .dark. The component Tailwind classes reference semantic tokens like bg-primary rather than specific colors like bg-blue-600. Changing your theme means changing the token values, not touching any component files.

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --border: 214.3 31.8% 91.4%;
  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --primary: 210 40% 98%;
  --primary-foreground: 222.2 47.4% 11.2%;
}

These values are in HSL without the hsl() wrapper, which lets Tailwind’s opacity modifiers work correctly (bg-primary/90 for 90% opacity).

The shadcn CLI includes a theme builder at its documentation site where you can pick base colors and preview the result, then copy the generated CSS variables into your globals.css. Theming takes minutes, not hours.

What’s Available in 2026

The component catalog has grown substantially from the early days. As of May 2026 you get:

Form and input: Button, Input, Textarea, Select, Checkbox, RadioGroup, Switch, Slider, DatePicker, Calendar, Form (built on react-hook-form + zod)

Overlay and navigation: Dialog, Sheet (side drawer), Popover, Tooltip, DropdownMenu, ContextMenu, NavigationMenu, Menubar, Tabs, Accordion

Data display: Table, Card, Badge, Avatar, Separator, Progress, Skeleton

Feedback: Alert, Toast (via sonner), AlertDialog

Charts: An integrated chart component using Recharts, styled to match your theme with no additional configuration needed

The chart components are particularly useful. Before these shipped, mixing Recharts or Chart.js with a shadcn-themed interface required manual color coordination. Now:

import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ChartContainer, ChartTooltip } from "@/components/ui/chart"

const data = [
  { month: "Jan", revenue: 4200 },
  { month: "Feb", revenue: 5800 },
  { month: "Mar", revenue: 4900 },
]

export function RevenueChart() {
  return (
    <ChartContainer className="h-[300px]">
      <BarChart data={data}>
        <CartesianGrid vertical={false} />
        <XAxis dataKey="month" />
        <YAxis />
        <ChartTooltip />
        <Bar dataKey="revenue" fill="var(--color-primary)" radius={4} />
      </BarChart>
    </ChartContainer>
  )
}

The chart picks up your theme colors automatically. That’s the point of the whole system.

Adding Components Selectively

The workflow is deliberate. You don’t install everything upfront:

# Start a new project
npx create-next-app@latest my-app --typescript --tailwind
cd my-app

# Initialize shadcn
npx shadcn@latest init

# Add components as you need them
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add data-table

Each component is copied into components/ui/. Your project only contains what you’ve added. There’s no tree-shaking needed because you only have what you’ve actually installed.

When a new shadcn version ships an updated dialog.tsx, you can choose to update (run the add command again with --overwrite), stay on your version, or apply specific changes manually. The update decision is yours, not forced by a dependency.

Customizing Without Breaking Things

The most common customization is adding variants. Say you need a success button variant:

// In your button.tsx
const buttonVariants = cva("...", {
  variants: {
    variant: {
      // existing variants...
      default: "bg-primary text-primary-foreground hover:bg-primary/90",
      // add this:
      success: "bg-green-600 text-white hover:bg-green-700",
    },
  },
})

That’s it. No extending a library’s type definitions. No overriding CSS specificity. You changed the file, and the change works everywhere the component is used.

For more substantial changes (adding a loading state with a spinner, changing the focus ring style across all inputs), make the change once in the component file and it propagates through the app.

When shadcn/ui Is Not the Right Choice

Copy-paste components only make sense if someone on the team will maintain them. The tradeoff is explicit: you get control, but you take on the ongoing work of keeping components up to date with your design requirements.

For a quick internal tool or a prototype, the overhead might not be worth it. Dropping in a full component library and accepting its opinions gets you to working faster.

For apps that need highly specialized components outside shadcn’s catalog (complex data grids, specialized data visualization, rich text editors), you’re mixing shadcn components with other libraries anyway. That works fine, but evaluate whether having two theming systems is worth it.

For teams without a frontend developer who cares about the component layer, the components will drift. shadcn/ui rewards teams that want to own their UI code.

Getting Started

npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npx shadcn@latest init

The init command asks about your preferences (base color, CSS variables or not), writes your tailwind.config.ts, and creates components/ui/. From there, add components as you need them.

The documentation at ui.shadcn.com is well-maintained and shows each component with its variants, usage examples, and the underlying Radix primitive. It’s worth bookmarking as the reference you’ll check the most.

The premise (that copying code into your project beats importing it from node_modules) sounded wrong in 2022. In 2026, it’s hard to argue with the results.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored