shadcn/ui has changed how React developers build component libraries. Rather than installing a black-box npm package, you copy components directly into your codebase — they’re yours to own, modify, and style. Paired with Tailwind CSS, the result is a design system that’s both flexible and consistent.
shadcn/ui is a collection of accessible, unstyled components built on Radix UI primitives. Instead of npm install shadcn-ui, you run a CLI that copies the source directly into your project — giving you full control over every pixel.
# Initialise in a Next.js project
npx shadcn-ui@latest init
# Add individual components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add data-table
The real power comes from combining shadcn/ui’s CSS variable system with a custom Tailwind config. Define your brand tokens once:
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%; /* Brand blue */
--primary-foreground: 210 40% 98%;
--accent: 210 40% 96.1%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
}
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
},
},
},
};
// components/ui/button.tsx (after shadcn init)
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-content-center rounded-md font-medium transition-colors focus-visible:outline-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
ghost: 'hover:bg-accent hover:text-accent-foreground',
// Add your own brand variant
brand: 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white hover:opacity-90',
},
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' },
}
);
// app/layout.tsx
import { ThemeProvider } from 'next-themes';
export default function Layout({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
The biggest win from this setup isn’t the visual consistency — it’s that every developer on your team has the same vocabulary. “Use the brand button variant” is unambiguous. That clarity pays dividends as your codebase scales.