shadcn/ui is Not a Component Library
The Standard Model is Broken
Every team I've worked on has had the same conversation at some point: "The design doesn't match what the library gives us, but overriding it is a nightmare." You end up in specificity wars, wrapping components in extra divs, and fighting the library's opinions with your own.
shadcn/ui takes a different position entirely: don't install a library, own the code.
What shadcn/ui Actually Is
When you run npx shadcn@latest add button, nothing gets added to node_modules. Instead, a Button.tsx file lands in your components/ui/ directory — with full source code, your project's Tailwind theme already wired in, and zero external dependency for the component itself.
// This is YOUR file now. Edit it freely.
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',
{
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 hover:bg-accent hover:text-accent-foreground',
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',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
);
The underlying primitives (Radix UI for accessibility, CVA for variants, Tailwind for styling) are still npm dependencies. But the component layer — the part you interact with daily — is yours.
Why This Changes Everything
Customization is the default, not the exception. Need a ghost button with a custom hover color that matches your brand? Edit the file. No sx props, no CSS overrides, no wrapper components. Just change the Tailwind class.
The abstraction boundary is honest. Traditional component libraries hide their internals. When something goes wrong, you're debugging minified code in node_modules. With shadcn/ui, the code is in your repo, readable and traceable.
Updates are deliberate. This is the tradeoff: shadcn/ui components don't auto-update. You re-run npx shadcn@latest add button and get a diff to review. Some teams see this as a downside. I see it as appropriate friction — UI should change intentionally.
The CSS Variable System
shadcn/ui pairs with a CSS variable design token system:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
}
Change the variables, the entire system shifts. Dark mode is a matter of flipping a class on <html>. The whole system is coherent because it's all pointing at the same tokens.
When Not to Use It
shadcn/ui is a great fit for product UI where you need control. It's less ideal if:
- You're prototyping fast and don't want to own components yet
- Your team doesn't have a design system and won't maintain the components
- You're using a framework that isn't React (though ports exist for Vue, Svelte, etc.)
For those cases, a traditional library like MUI or Mantine might serve you better. But if you've ever felt constrained by a component library, shadcn/ui is worth a serious look.