← All articles
FRONTEND shadcn/ui: Copy-Paste Component Library for React an... 2026-03-04 · 5 min read · shadcn · react · tailwind

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

Frontend 2026-03-04 · 5 min read shadcn react tailwind components ui-library accessibility

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.

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.

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.