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
7 min read
Sponsored
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
More from this category
More from Web Development
CSS Anchor Positioning: Tooltips and Popovers Without JavaScript
gRPC in 2026: When to Use It Instead of REST or GraphQL
k6 Load Testing: Performance Testing Your APIs Before Users Find the Problems
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.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored