Shadcn/UI: Finally Built UI Components That Don't Suck

Tried every React UI library out there. Shadcn/UI is different - you own the code. Copy-paste components, customize everything, ship faster. Here's my complete guide.

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 (
    
      
        
      
      
        
          Are you sure?
        
        
This action cannot be undone.
) }

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 (
    
( Username )} /> ( Email )} /> ) }

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.

📚 Recommended Reading