shadcn/ui changed how I think about component libraries. After using it across 4 production projects, here's what makes it different and when it's the right choice.
It's not a library. It's a collection.
The core insight: shadcn/ui doesn't install into node_modules. You copy components directly into your project. Each component is a single file in your components/ui/ directory that you own and can modify.
npx shadcn@latest init
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
After running these commands, you have:
components/ui/
button.tsx # You own this file
dialog.tsx # Modify it however you want
dropdown-menu.tsx
No import { Button } from '@shadcn/ui'. Instead: import { Button } from '@/components/ui/button'. The component is your code now.
Why this matters in practice
1. No version lock-in
With a traditional library (MUI, Chakra, Mantine), upgrading from v4 to v5 is a project in itself. Breaking changes cascade through your entire app. With shadcn/ui, there's nothing to upgrade. You copied the code. It's frozen at whatever point you copied it, and you modify it as needed.
2. No bundle bloat
You only have the components you actually use. No tree-shaking needed because there's no package — just the files you explicitly added. A typical shadcn/ui setup adds 20-50KB to your bundle depending on how many components you use.
3. Full customization without fighting the framework
// components/ui/button.tsx — YOUR file
// Want to change the default border radius? Just change it.
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-lg ...", // was rounded-md
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
// Add your own variant
brand: "bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25",
},
},
}
)
Try adding a custom variant to MUI's Button component. It's possible but involves createTheme, styleOverrides, and TypeScript module augmentation. With shadcn/ui, you just edit the file.
The components that matter most
After 4 projects, these are the ones I install on every project:
Always install:
-
button— foundational, well-designed variants -
input+label— form building blocks -
dialog— modal dialogs done right -
dropdown-menu— right-click and action menus -
toast(viasonner) — notifications -
card— content containers -
table— data display with sorting and filtering patterns -
form— React Hook Form integration with Zod validation -
tabs— tabbed interfaces -
select— custom selects that actually work
Install when needed:
-
command— command palette (Cmd+K search) -
sheet— slide-over panels -
popover— positioned floating content -
calendar+date-picker— date selection -
data-table— full-featured data tables with TanStack Table -
carousel— image/content carousels
Skip unless specific need:
-
accordion— rarely needed if you design well -
navigation-menu— overengineered for most navs -
hover-card— niche use case
The form pattern
This is where shadcn/ui really shines — the Form component integrates React Hook Form with Zod validation:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
const schema = z.object({
email: z.string().email('Invalid email'),
name: z.string().min(2, 'Name must be at least 2 characters'),
});
export function SignupForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { email: '', name: '' },
});
function onSubmit(values: z.infer<typeof schema>) {
// Type-safe, validated values
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage /> {/* Automatic Zod error messages */}
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Sign up</Button>
</form>
</Form>
);
}
Validation errors appear automatically under each field. The submit handler receives typed, validated data. The UX is polished without writing any error-handling UI code.
Theming with CSS variables
shadcn/ui uses CSS custom properties for theming, not a JavaScript theme object:
/* globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
}
Changing your primary color changes every component simultaneously. Dark mode is one CSS class toggle. No theme provider wrapper needed.
For custom brand theming:
:root {
--primary: 192 100% 50%; /* Cyan #00BFFF */
--primary-foreground: 0 0% 0%; /* Black text on cyan */
--accent: 192 100% 50%;
}
Every button, input focus ring, active tab, and link color updates automatically.
The data table pattern
For dashboards, the data-table recipe with TanStack Table is excellent:
import { DataTable } from '@/components/ui/data-table';
import { ColumnDef } from '@tanstack/react-table';
type Payment = {
id: string;
amount: number;
status: 'pending' | 'processing' | 'success' | 'failed';
email: string;
};
const columns: ColumnDef<Payment>[] = [
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'amount', header: 'Amount',
cell: ({ row }) => `$${(row.getValue('amount') as number).toFixed(2)}` },
{ accessorKey: 'status', header: 'Status',
cell: ({ row }) => (
<Badge variant={row.getValue('status') === 'success' ? 'default' : 'destructive'}>
{row.getValue('status')}
</Badge>
)},
];
export function PaymentsTable({ data }: { data: Payment[] }) {
return <DataTable columns={columns} data={data} />;
}
Sorting, filtering, pagination — all built in. The column definitions are type-safe. Adding a new column is one object in the array.
When NOT to use shadcn/ui
- You need a comprehensive design system out of the box — MUI or Ant Design ship with more components and patterns
- Your team doesn't know Tailwind — shadcn/ui is built on Tailwind. No Tailwind knowledge = painful
- You need React Native — shadcn/ui is web-only (look at Tamagui or NativeWind)
- You want zero configuration — the init process and Tailwind setup is a few minutes, not zero
The honest take
shadcn/ui is the right choice for most new React projects in 2026. The ownership model (copy, don't install) solves the two biggest problems with component libraries: upgrade hell and customization friction.
The main trade-off is that you're responsible for your components. If a shadcn/ui component gets a bug fix upstream, you have to manually update your copy. In practice, this rarely matters because the components are stable and well-tested.
For the AI SaaS Starter Kit, every dashboard component is built with shadcn/ui — data tables, forms, cards, dialogs, toasts. The theming is configured with the brand colors from day one. You get the component ownership benefits without the setup time.
Full component catalog and examples: ui.shadcn.com. The "Examples" section has complete page layouts (dashboard, authentication, settings) that you can copy directly.
Top comments (0)