ShadCN UI has fundamentally changed how React developers think about component libraries. Instead of installing an opinionated npm package that controls your styling and upgrade path, ShadCN gives you beautifully designed, accessible components that you copy directly into your project and own completely. In 2026, ShadCN combined with Tailwind CSS v4 and Next.js has become the dominant UI stack for new React applications — from startup MVPs to enterprise dashboards. This guide covers everything from initial setup to building a full design system with ShadCN as the foundation.
What Makes ShadCN Different: Copy-Paste, Not npm Install
Traditional component libraries like Material UI and Chakra UI ship as npm packages. You install them, import components, and customize through their theming APIs. This approach has a critical tradeoff: convenience in exchange for control. When you need to modify a component's internal structure, override deeply nested styles, or diverge from the library's design language, you fight against the abstraction. ShadCN takes the opposite approach.
- You own the code: Components are copied into your `components/ui/` directory — no node_modules dependency
- Full customization: Modify any component's HTML structure, styling, or behavior directly
- No version lock-in: No breaking changes from library updates; you control when and how to change components
- Smaller bundle: You only include the components you use, and tree-shaking is automatic since they're your own files
- Built on primitives: ShadCN uses Radix UI for accessibility and behavior, Tailwind CSS for styling — both best-in-class
- Type-safe: Full TypeScript support with properly typed props and variants
ShadCN Is Not a Component Library
The creator, shadcn (Saad Haj Bakry), explicitly states: "This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps." This distinction matters because it changes your mental model — you're not consuming a library, you're using a high-quality starting point that becomes your code.
ShadCN + Tailwind CSS v4 + Next.js: The Dominant UI Stack
ShadCN was designed for the Tailwind CSS and Next.js ecosystem, and the release of Tailwind CSS v4 has made this combination even more powerful. Tailwind v4's native CSS variables, zero-config content detection, and 10x faster builds complement ShadCN's approach perfectly. Next.js provides the application framework with server components, streaming, and optimized builds.
Setting Up ShadCN in a New Project
# Create a new Next.js project
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
# Initialize ShadCN UI
npx shadcn@latest init
# The init wizard will ask:
# - Style: Default or New York (visual style preset)
# - Base color: Slate, Gray, Zinc, Neutral, or Stone
# - CSS variables for theming: Yes (recommended)
# Add individual components as needed
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add table
npx shadcn@latest add command
npx shadcn@latest add dropdown-menu
npx shadcn@latest add toast
# Add multiple components at once
npx shadcn@latest add button card dialog form input
# See all available components
npx shadcn@latest add// After running `npx shadcn@latest add button`, you get:
// components/ui/button.tsx — This is YOUR code now
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// Base styles applied to all buttons
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };Key Components: Dialog, Command Palette, Data Table, and Form
ShadCN includes over 50 components, but certain ones are foundational to modern web applications. These four components demonstrate the power and flexibility of the ShadCN approach.
Dialog Component
// A confirmation dialog built with ShadCN Dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export function DeleteConfirmDialog({
onConfirm,
itemName
}: {
onConfirm: () => void;
itemName: string;
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">Delete</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Delete {itemName}?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete
the item and all associated data.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive" onClick={onConfirm}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Command Palette (cmdk)
// A searchable command palette — the power-user feature every app needs
import { useEffect, useState } from 'react';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import { useRouter } from 'next/navigation';
export function CommandPalette() {
const [open, setOpen] = useState(false);
const router = useRouter();
// Toggle with Cmd+K / Ctrl+K
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => !prev);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
const navigate = (path: string) => {
setOpen(false);
router.push(path);
};
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
<CommandItem onSelect={() => navigate('/dashboard')}>
Dashboard
</CommandItem>
<CommandItem onSelect={() => navigate('/projects')}>
Projects
</CommandItem>
<CommandItem onSelect={() => navigate('/settings')}>
Settings
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
<CommandItem onSelect={() => navigate('/projects/new')}>
Create New Project
</CommandItem>
<CommandItem onSelect={() => navigate('/team/invite')}>
Invite Team Member
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}Customizing the Theme and Design Tokens
ShadCN uses CSS variables for theming, which means you can completely change the look and feel of every component by modifying a few CSS custom properties. With Tailwind CSS v4, these variables integrate naturally with the @theme directive for a seamless theming experience.
/* app/globals.css — Define your design tokens */
@import "tailwindcss";
@theme {
/* ShadCN uses HSL color values for flexibility */
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(240 10% 3.9%);
--color-card: hsl(0 0% 100%);
--color-card-foreground: hsl(240 10% 3.9%);
--color-popover: hsl(0 0% 100%);
--color-popover-foreground: hsl(240 10% 3.9%);
--color-primary: hsl(240 5.9% 10%);
--color-primary-foreground: hsl(0 0% 98%);
--color-secondary: hsl(240 4.8% 95.9%);
--color-secondary-foreground: hsl(240 5.9% 10%);
--color-muted: hsl(240 4.8% 95.9%);
--color-muted-foreground: hsl(240 3.8% 46.1%);
--color-accent: hsl(240 4.8% 95.9%);
--color-accent-foreground: hsl(240 5.9% 10%);
--color-destructive: hsl(0 84.2% 60.2%);
--color-destructive-foreground: hsl(0 0% 98%);
--color-border: hsl(240 5.9% 90%);
--color-input: hsl(240 5.9% 90%);
--color-ring: hsl(240 5.9% 10%);
--radius: 0.5rem;
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-background: hsl(240 10% 3.9%);
--color-foreground: hsl(0 0% 98%);
--color-card: hsl(240 10% 3.9%);
--color-card-foreground: hsl(0 0% 98%);
--color-primary: hsl(0 0% 98%);
--color-primary-foreground: hsl(240 5.9% 10%);
--color-secondary: hsl(240 3.7% 15.9%);
--color-secondary-foreground: hsl(0 0% 98%);
--color-muted: hsl(240 3.7% 15.9%);
--color-muted-foreground: hsl(240 5% 64.9%);
--color-accent: hsl(240 3.7% 15.9%);
--color-accent-foreground: hsl(0 0% 98%);
--color-destructive: hsl(0 62.8% 30.6%);
--color-destructive-foreground: hsl(0 0% 98%);
--color-border: hsl(240 3.7% 15.9%);
--color-input: hsl(240 3.7% 15.9%);
--color-ring: hsl(240 4.9% 83.9%);
}
}
/* Custom brand theme example — change 6 variables to rebrand everything */
.theme-brand {
--color-primary: hsl(262 83.3% 57.8%); /* Purple brand */
--color-primary-foreground: hsl(0 0% 100%);
--color-ring: hsl(262 83.3% 57.8%);
--radius: 0.75rem; /* Rounder corners */
}Building a Design System with ShadCN
ShadCN components are an excellent foundation for a custom design system. Because you own the code, you can extend components with domain-specific variants, enforce brand consistency, and create higher-level composite components that encapsulate your application's patterns.
// Extend the Button with domain-specific variants
// components/ui/button.tsx — add your own variants
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
// ... existing ShadCN variants ...
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// Custom brand variants
brand: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white shadow-lg hover:shadow-xl hover:scale-[1.02] transition-all',
success: 'bg-emerald-600 text-white shadow-sm hover:bg-emerald-700',
warning: 'bg-amber-500 text-white shadow-sm hover:bg-amber-600',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
xl: 'h-12 rounded-lg px-10 text-base', // Custom larger size
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
// Higher-level composite component
// components/stat-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
interface StatCardProps {
title: string;
value: string | number;
description?: string;
trend?: 'up' | 'down' | 'neutral';
trendValue?: string;
className?: string;
}
export function StatCard({ title, value, description, trend, trendValue, className }: StatCardProps) {
return (
<Card className={cn('relative overflow-hidden', className)}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
{trend && trendValue && (
<span className={cn(
'text-xs font-medium',
trend === 'up' && 'text-emerald-600',
trend === 'down' && 'text-red-600',
trend === 'neutral' && 'text-muted-foreground'
)}>
{trend === 'up' ? '+' : trend === 'down' ? '-' : ''}{trendValue}
</span>
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground mt-1">{description}</p>
)}
</CardContent>
</Card>
);
}ShadCN in Monorepos: The Turborepo Pattern
For teams building multiple applications (marketing site, dashboard, admin panel), sharing ShadCN components through a monorepo is the recommended approach. Turborepo is the most popular monorepo tool in the Next.js ecosystem, and ShadCN works seamlessly with it.
# Monorepo structure with shared ShadCN components
my-monorepo/
packages/
ui/ # Shared ShadCN components
components/
ui/
button.tsx
card.tsx
dialog.tsx
...
lib/
utils.ts # cn() utility
package.json # @acme/ui
tsconfig.json
config-tailwind/ # Shared Tailwind config
index.ts
package.json # @acme/tailwind-config
apps/
web/ # Marketing site
app/
package.json # depends on @acme/ui
dashboard/ # Admin dashboard
app/
package.json # depends on @acme/ui
turbo.json
package.json// packages/ui/package.json
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": "./components/ui/button.tsx",
"./card": "./components/ui/card.tsx",
"./dialog": "./components/ui/dialog.tsx",
"./lib/utils": "./lib/utils.ts",
"./globals.css": "./globals.css"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@acme/tailwind-config": "workspace:*",
"react": "^19.0.0",
"typescript": "^5.7.0"
}
}Accessibility Built-In: Radix UI Primitives
One of ShadCN's greatest strengths is its accessibility foundation. Every interactive component is built on Radix UI primitives, which implement the WAI-ARIA design patterns specification. This means keyboard navigation, screen reader support, focus management, and ARIA attributes work correctly out of the box — without you having to implement any of it.
- Keyboard navigation: All components support Tab, Enter, Space, Escape, and Arrow key interactions per WAI-ARIA spec
- Screen reader support: Proper ARIA roles, labels, and descriptions are built into every primitive
- Focus management: Dialogs trap focus, menus manage roving focus, popovers return focus on close
- Animation-safe: Reduced motion preferences are respected; animations can be disabled without breaking functionality
- Color contrast: Default themes meet WCAG 2.1 AA contrast ratios
- Form accessibility: Labels are properly associated with inputs, error messages use aria-describedby, required fields are marked with aria-required
// ShadCN's Dialog automatically handles accessibility
// You don't need to add ARIA attributes manually
<Dialog>
{/* DialogTrigger sets aria-haspopup, aria-expanded, aria-controls */}
<DialogTrigger asChild>
<Button>Open Settings</Button>
</DialogTrigger>
{/* DialogContent automatically:
- Sets role="dialog" and aria-modal="true"
- Traps focus inside the dialog
- Closes on Escape key
- Returns focus to trigger on close
- Associates title with aria-labelledby
- Associates description with aria-describedby */}
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Manage your account settings and preferences.
</DialogDescription>
</DialogHeader>
{/* Dialog content here */}
</DialogContent>
</Dialog>
{/* Dropdown menu with full keyboard support */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Options</Button>
</DropdownMenuTrigger>
{/* DropdownMenuContent automatically:
- Supports Arrow Up/Down navigation
- Supports type-ahead search
- Closes on Escape or click outside
- Uses role="menu" and role="menuitem" */}
<DropdownMenuContent>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>ShadCN vs Material UI vs Chakra UI
Choosing a component system depends on your project's needs, team size, and customization requirements. Here's an honest comparison based on building production applications with all three at Jishu Labs.
- ShadCN UI: Best for teams that want full control and use Tailwind CSS. Zero runtime overhead, complete customization, modern defaults. Tradeoff: You maintain the components yourself and need to track updates manually. Best for: Custom-branded products, startups, Tailwind-based teams.
- Material UI (MUI): The most comprehensive library with the largest component collection. Implements Google's Material Design with extensive theming. Tradeoff: Larger bundle size, opinionated styling that's harder to override, and the Emotion/styled-components runtime. Best for: Enterprise apps that want Material Design, teams needing a massive component catalog.
- Chakra UI: Excellent developer experience with a prop-based styling API. Good defaults and easy customization. Tradeoff: Smaller component selection than MUI, runtime CSS-in-JS overhead, and version 3 introduced breaking changes. Best for: Teams that prefer prop-based styling over utility classes.
- Bundle size comparison: ShadCN adds only the components you use (typically 5-20KB total). MUI adds 80-150KB+ for core. Chakra adds 60-100KB+ for core.
- Customization depth: ShadCN gives you the source code to modify directly. MUI and Chakra require working within their theming APIs, which can be limiting for non-standard designs.
Our Recommendation for 2026
For new React projects, we recommend ShadCN UI with Tailwind CSS v4. The combination of full code ownership, excellent accessibility, minimal bundle size, and the vibrant Tailwind ecosystem makes it the strongest choice. If you're building an internal tool and want maximum component coverage with minimal customization, MUI remains a solid option. If you prefer prop-based styling and are not using Tailwind, Chakra UI v3 is worth evaluating.
Common Patterns and Recipes
Here are practical patterns that appear in almost every ShadCN-based application. These recipes combine multiple components into reusable solutions for common UI requirements.
// Pattern 1: Form with validation using ShadCN Form + React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const profileSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
bio: z.string().max(500, 'Bio must be under 500 characters').optional(),
});
type ProfileFormValues = z.infer<typeof profileSchema>;
export function ProfileForm() {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema),
defaultValues: { name: '', email: '', bio: '' },
});
async function onSubmit(data: ProfileFormValues) {
// Handle form submission
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormDescription>Your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving...' : 'Save Profile'}
</Button>
</form>
</Form>
);
}Frequently Asked Questions
Frequently Asked Questions
How do I update ShadCN components when new versions are released?
Since ShadCN components live in your codebase, updates are opt-in. Run `npx shadcn@latest diff` to see what changed in the upstream components since you added them. You can then manually apply changes you want, or re-add a component with `npx shadcn@latest add button --overwrite` to get the latest version (this overwrites your customizations, so review first). For most teams, checking the diff monthly and selectively applying bug fixes or accessibility improvements is the best approach.
Can I use ShadCN with CSS Modules or styled-components instead of Tailwind?
ShadCN is built specifically for Tailwind CSS and relies on Tailwind utility classes throughout. Using it with CSS Modules or styled-components would require rewriting every component's styling, which defeats the purpose. If your project uses styled-components, consider Chakra UI or MUI instead. If your project uses CSS Modules, you could adopt Tailwind incrementally and use ShadCN for new components while keeping existing components on CSS Modules.
Is ShadCN production-ready for enterprise applications?
Yes. ShadCN's foundation — Radix UI primitives — is used in production by companies like Vercel, Supabase, and Linear. The component code you get is well-tested, accessible, and follows React best practices. Because you own the code, you have full control over quality, testing, and security auditing. Many enterprise teams at companies we work with use ShadCN as their design system foundation. The key advantage for enterprise is no dependency on a third-party library's release cycle or breaking changes.
How does ShadCN handle dark mode and theming?
ShadCN uses CSS variables for all colors, which makes dark mode trivial. The default setup includes both light and dark theme variables. You can toggle themes using the `next-themes` package (recommended) or a simple class toggle on the HTML element. For custom themes beyond light/dark, define additional variable sets under CSS classes (e.g., `.theme-brand`, `.theme-blue`) and apply them to any container. Every ShadCN component automatically responds to theme changes because they reference CSS variables, not hardcoded colors.
Conclusion
ShadCN UI has earned its position as the default component system for the React and Tailwind ecosystem by giving developers what they actually want: beautiful, accessible components that they fully own and control. The copy-paste model feels counterintuitive at first, but in practice it eliminates the frustrations of fighting against library abstractions, dealing with breaking version upgrades, and shipping bloated bundles. Combined with Tailwind CSS v4 and Next.js, ShadCN provides the foundation for building world-class user interfaces in 2026.
Need help building a design system or migrating to ShadCN UI? Contact Jishu Labs for expert frontend consulting, component architecture, and design system engineering.
About Emily Rodriguez
Emily Rodriguez is a Senior Engineer at Jishu Labs with expertise in testing strategies, CI/CD pipelines, and frontend architecture.