shadcn/ui
01 / 02

shadcn/ui Fundamentals: The Copy-Paste Model

shadcn/ui: The Copy-Paste Model

shadcn/ui is a collection of pre-styled, accessible React components built on top of Radix UI primitives and Tailwind CSS. Unlike a traditional npm component library, components are distributed as copy-pasteable source code -- via a CLI -- directly into your project, not installed as an opaque node_modules dependency.

Adding a Component

npx shadcn init            # generates components.json based on your
                            # project setup (Tailwind config, TS usage)

npx shadcn add button dialog card
# Copies the actual .tsx source for these components directly into
# components/ui/ -- NOT added to package.json as a versioned dependency

Why Copy-Paste Instead of npm install

Since the component's actual source lives directly in your project, you can freely read, modify, and fully customize any part of it -- styling, behavior, structure -- without fighting a traditional library's theming API or waiting for an upstream maintainer. The tradeoff: your project now owns and must maintain its own copy of that code.

Built on Radix UI + Tailwind

// components/ui/button.tsx -- simplified
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

// cva structures a component's variants (variant, size) and the
// Tailwind classes each combination applies -- avoids a tangle of
// manual conditional className logic
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground',
        destructive: 'bg-destructive text-destructive-foreground',
      },
      size: {
        sm: 'h-8 px-3',
        lg: 'h-10 px-8',
      },
    },
  }
)

export function Button({ className, variant, size, ...props }) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

Interactive Components Inherit Radix's Accessibility

A shadcn/ui Dialog correctly traps focus and supports Escape-to-close not because shadcn/ui reimplemented that logic, but because it's built directly on top of @radix-ui/react-dialog -- shadcn/ui's specific contribution is the visual styling layered on top of an already-correct accessible foundation.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free