← All articles
white printer paper with blue text

shadcn/ui: Copy-Paste Component Library for React and Tailwind

Frontend 2026-03-04 · 5 min read shadcn react tailwind components ui-library accessibility
By DevTools Guide Editorial TeamSoftware engineers and developer advocates covering tools, workflows, and productivity for modern development teams.

Most UI libraries ship as npm packages — you install them, use their components, and live with their constraints. shadcn/ui takes a different approach: it's a collection of components you copy directly into your codebase. No package to install, no version upgrades, no API constraints. You own the code.

Photo by Markus Spiske on Unsplash

This makes shadcn/ui unusually adaptable. Every component is a starting point, not a black box.

The Model: Copy-Paste, Not Install

Traditional library:

npm install @some-ui/button
# Use it, but can't modify internals without forking

shadcn/ui:

npx shadcn@latest add button
# Creates src/components/ui/button.tsx in your project
# It's your code now — read it, modify it, own it

Each component is built on:

Installation

Prerequisites: React project with Tailwind CSS configured.

# Initialize shadcn/ui in your project
npx shadcn@latest init

The init command asks about your project structure:

It adds dependencies (radix-ui packages, clsx, tailwind-merge, class-variance-authority) and configures tailwind.config.js.

Adding Components

Add components individually as you need them:

npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add select
npx shadcn@latest add table

Or add multiple at once:

npx shadcn@latest add button input label form

Each add command creates one or more files in your components/ui/ directory. These are your files — commit them, modify them, and don't treat them as sacred.

Like what you're reading? Subscribe to DevTools Guide — free weekly guides in your inbox.

Using Components

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

export function LoginForm() {
  return (
    <form className="space-y-4">
      <div>
        <Label htmlFor="email">Email</Label>
        <Input id="email" type="email" placeholder="[email protected]" />
      </div>
      <div>
        <Label htmlFor="password">Password</Label>
        <Input id="password" type="password" />
      </div>
      <Button type="submit" className="w-full">Sign in</Button>
    </form>
  );
}

Component Variants

shadcn/ui components use cva (class-variance-authority) for variants. The Button component, for example:

// src/components/ui/button.tsx (your file — look at it!)
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium ...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground ...",
        outline: "border border-input bg-background ...",
        secondary: "bg-secondary text-secondary-foreground ...",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

Usage:

<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline" size="sm">Small</Button>
<Button variant="ghost" size="icon"><Icon /></Button>

The Form Component

shadcn/ui's Form component wraps React Hook Form with Zod validation, providing accessible form fields with error messages:

npx shadcn@latest add form
npm install react-hook-form @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
  Form, FormField, FormItem, FormLabel,
  FormControl, 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'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type FormData = z.infer<typeof schema>;

export function LoginForm() {
  const form = useForm<FormData>({ resolver: zodResolver(schema) });

  function onSubmit(data: FormData) {
    console.log(data);
  }

  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="[email protected]" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign in</Button>
      </form>
    </Form>
  );
}

Theming with CSS Variables

shadcn/ui uses CSS custom properties for theming. The default globals.css includes variables for light and dark mode:

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  /* ... more variables */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... dark mode overrides */
}

To change your brand color, update --primary and --primary-foreground. The change propagates across all components that use those variables.

Use the shadcn/ui themes generator to visually select and export a color scheme.

Dark Mode

Add dark mode support with next-themes (or any class-based dark mode solution):

npm install next-themes
// app/providers.tsx
import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}
// components/theme-toggle.tsx
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
    </Button>
  );
}

Modifying Components

This is where shadcn/ui shines. Because components live in your codebase, you can change anything:

// src/components/ui/button.tsx
// Add a new variant:
const buttonVariants = cva("...", {
  variants: {
    variant: {
      // ... existing variants ...
      brand: "bg-purple-600 text-white hover:bg-purple-700",  // your addition
    },
  },
});
// Usage:
<Button variant="brand">Upgrade Now</Button>

You can also create new components that compose existing ones:

// src/components/ui/icon-button.tsx
import { Button, ButtonProps } from './button';
import { LucideIcon } from 'lucide-react';

interface IconButtonProps extends ButtonProps {
  icon: LucideIcon;
  label: string;
}

export function IconButton({ icon: Icon, label, ...props }: IconButtonProps) {
  return (
    <Button {...props}>
      <Icon className="mr-2 h-4 w-4" />
      {label}
    </Button>
  );
}

Accessibility Out of the Box

Radix UI (the underlying primitives) handles ARIA attributes, focus trapping, keyboard navigation, and screen reader support for complex components:

You get accessibility without thinking about it — unless you modify components in ways that break the Radix primitives' guarantees.

shadcn/ui vs Alternatives

Library Approach Customization Bundle size
shadcn/ui Copy-paste Full — you own it Zero (no npm dep)
Mantine npm package Config + CSS vars ~200KB
Chakra UI npm package Theme system ~170KB
Ant Design npm package Theme tokens ~500KB+
Headless UI npm package Full (no styles) ~20KB

Summary

shadcn/ui is the right default component library for new React + Tailwind projects. The copy-paste model means you get beautiful, accessible components as a starting point — not a dependency you're locked into.

Initialize it with npx shadcn@latest init, add components as you need them, and treat the generated files as your own code from day one.

Get free weekly tips in your inbox. Subscribe to DevTools Guide