Why I switched from Material-UI
Been building React apps for years. Used Material-UI, Ant Design, Chakra UI - you name it. They all had the same problem: locked-in dependencies.
Want to change a button style? Fight the library's CSS overrides. Need to customize a component? Read through pages of prop APIs. Want to remove unused code? Tree-shaking was hit-or-miss.
Then found Shadcn/UI. It's not a component library - it's a collection of copy-paste components you add to your project. You own the code. You can modify it. No dependencies to manage.
Built my last SaaS dashboard in half the time. Customization was trivial. Bundle size stayed small because I only added what I needed.
What makes Shadcn/UI different
Not an npm package. You copy component code directly into your project. Based on Radix UI (accessible primitives) + Tailwind CSS (styling). Full control, zero dependencies, customizable forever. Your code, your rules.
Installation and setup
Shadcn/UI works with React, Next.js, Vite, and more. Here's my Next.js setup.
Prerequisites
- React 18+ or Next.js 13+
- Tailwind CSS configured
- Node.js 18+
Step 1: Initialize your project
# Create Next.js app
npx create-next-app@latest my-app
cd my-app
# Or use existing project
Step 2: Configure Tailwind CSS
Shadcn/UI needs Tailwind configured with custom paths.
// tailwind.config.js
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
// ... more color tokens
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}
Step 3: Add CSS variables
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode colors */
}
}
}
Step 4: Install Shadcn/UI CLI
# Initialize Shadcn/UI
npx shadcn-ui@latest init
# Follow the prompts:
# - TypeScript: Yes
# - Style: Default
# - Base color: Slate
# - CSS variables: Yes
Step 5: Add components
# Add specific components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add dialog
# Or add multiple at once
npx shadcn-ui@latest add button card input
Components are copied to components/ui directory. You own the code.
Essential components I use daily
These components end up in every project I build.
1. Button - Customizable by default
Variants, sizes, icons - all built with Tailwind classes you can modify.
import { Button } from "@/components/ui/button"
export default function Page() {
return (
)
}
Want custom variant? Just edit components/ui/button.tsx and add your Tailwind classes.
2. Card - Better than div soup
Structured card layout with header, content, footer. Perfect for dashboards.
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export function StatsCard() {
return (
Total Users
12,345
+20% from last month
)
}
3. Form components - Input, Select, Checkbox
Form elements with consistent styling and built-in validation support.
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
export function LoginForm() {
return (
)
}
4. Dialog - Modal without the headache
Accessible dialog component with backdrop and keyboard support built-in.
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
export function ConfirmDialog() {
return (
)
}
Built on Radix UI Dialog - accessibility handled for you.
5. Table - Data tables that work
Clean table component with built-in styling for headers and rows.
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function UserTable() {
const users = [
{ id: 1, name: "John", email: "john@example.com" },
{ id: 2, name: "Jane", email: "jane@example.com" },
]
return (
Name
Email
{users.map((user) => (
{user.name}
{user.email}
))}
)
}
Customizing components
Real power: you can modify anything because it's your code.
Adding custom variants
Need a new button variant? Just edit the component file.
// components/ui/button.tsx
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 bg-background hover:bg-accent hover:text-accent-foreground",
// Add your custom variant
success: "bg-green-600 text-white hover:bg-green-700",
},
},
}
)
Changing component behavior
Want to add auto-complete to input? Modify the component directly.
// components/ui/input.tsx
import * as React from "react"
export interface InputProps
extends React.InputHTMLAttributes {
autocompleteOptions?: string[]
}
const Input = React.forwardRef(
({ className, type, autocompleteOptions, ...props }, ref) => {
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
return (
{autocompleteOptions && showAutocomplete && (
{autocompleteOptions.map((option) => (
{option}
))}
)}
)
}
)
Theming with CSS variables
Change entire app theme by modifying CSS variables.
/* Custom brand color */
:root {
--primary: 210 100% 50%; /* Blue brand color */
--primary-foreground: 0 0% 100%;
}
/* Dark mode toggle */
.dark {
--primary: 210 100% 60%;
--primary-foreground: 222.2 47.4% 11.2%;
}
All components use these variables automatically. Theme entire app in one place.
Shadcn/UI vs other libraries
Quick comparison with popular React UI libraries.
| Feature | Shadcn/UI | Material-UI | Chakra UI |
|---|---|---|---|
| Installation | Copy-paste code | npm install | npm install |
| Bundle size | Only what you use | Tree-shakeable | Medium |
| Customization | Edit code directly | Theme config | Theme config |
| Dependencies | Radix UI + class-variance-authority | @mui/material | @chakra-ui/react |
| Accessibility | Built-in (Radix UI) | Good | Good |
| Learning curve | Low (if you know Tailwind) | Moderate | Low |
When to choose Shadcn/UI
- You want full control over component code
- You're comfortable editing Tailwind CSS classes
- You need heavy customization
- You want minimal bundle size
- You're building with Next.js or React
When to use other libraries
- You need pre-built components fast without customization
- You don't want to manage component code
- You prefer prop-based customization
- You're not using Tailwind CSS
Common UI patterns
Real UI patterns I've built with Shadcn/UI.
1. Data table with sorting and filtering
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
export function SortableTable({ data }) {
const [sortConfig, setSortConfig] = React.useState({ key: null, direction: 'asc' })
const [filter, setFilter] = React.useState('')
const filteredData = data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
const sortedData = React.useMemo(() => {
if (!sortConfig.key) return filteredData
return [...filteredData].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'asc' ? -1 : 1
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'asc' ? 1 : -1
return 0
})
}, [filteredData, sortConfig])
return (
setFilter(e.target.value)}
className="mb-4"
/>
setSortConfig({ key: 'name', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}>
Name {sortConfig.key === 'name' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
Email
{sortedData.map(item => (
{item.name}
{item.email}
))}
)
}
2. Form with validation
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
const formSchema = z.object({
username: z.string().min(2, "Username must be at least 2 characters"),
email: z.string().email("Invalid email"),
})
export function ProfileForm() {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer) {
console.log(values)
}
return (
)
}
3. Command palette (Cmd+K)
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { useEffect, useState } from "react"
export function CommandPalette() {
const [open, setOpen] = useState(false)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
return (
No results found.
Calendar
Search Email
Settings
)
}
Common issues and fixes
Stuff that breaks and how to fix it.
Issue: Components don't have styles
Fix: Make sure Tailwind is configured correctly. Check that tailwind.config.js includes your components directory. Verify CSS variables are defined in globals.css.
Issue: CLI init command fails
Fix: Ensure you're in a Next.js or React project. Check that package.json exists. Update Node.js to v18+. Clear cache with rm -rf node_modules .next and retry.
Issue: Component not found after adding
Fix: Check import path. Shadcn/UI uses @/components/ui/.... Ensure your tsconfig.json has path aliases set up correctly.
Issue: Dark mode not working
Fix: Make sure darkMode: ["class"] is in Tailwind config. Add class="dark" to <html> element when toggling dark mode. Verify dark mode CSS variables exist.
Issue: TypeScript errors in components
Fix: Update types: npm install -D @types/react @types/react-dom. Ensure tsconfig.json includes "jsx": "preserve" and proper path aliases.
Issue: Component updates overwrite my changes
Fix: Shadcn/UI won't overwrite existing components. Your changes are safe. But if you re-add a component, you'll need to merge changes manually. Consider backing up customized components.
Best practices from production use
Lessons learned after shipping multiple projects with Shadcn/UI.
1. Only add what you need
Don't add all components at once. Start with Button, Input, Card - basics. Add others as needed. Keeps codebase clean.
2. Customize early
If you need custom variants, add them before building many components. Changing button styles after using it in 50 places sucks.
3. Create wrapper components
For app-specific UI patterns, create wrapper components:
// components/app-button.tsx
import { Button } from "@/components/ui/button"
export function AppButton({ children, ...props }) {
return (
)
}
4. Version control your components
Since you own the code, track changes in Git. Easy to revert customizations that don't work.
5. Document your custom variants
Create a storybook or docs page showing your custom component variants. Future you will thank present you.
6. Use with component libraries
Shadcn/UI doesn't have every component. Combine with other libraries for complex stuff like data grids or rich text editors.
Bottom line
Shadcn/UI isn't for everyone. If you want pre-built components and don't care about customization, Material-UI or Chakra UI might be better.
But if you want full control, minimal bundle size, and the ability to customize anything, Shadcn/UI is fantastic. It's how component libraries should work - you own the code, you modify it, you ship faster.
Built 3 SaaS apps with it now. Each time, customization was trivial. No fighting against the library's architecture. No bundle size bloat. Just clean, accessible components that work.
If you're starting a new React/Next.js project, give Shadcn/UI a shot. Add a few components, see how it feels. Worst case: you learned a new approach. Best case: you found your go-to UI solution.