Shadcn
Last modified on Wed 25 Jun 2025

ShadCN UI

ShadCN UI (often written simply as shadcn/ui) is a set of open-source React component templates that sit on top of Radix UI primitives and Tailwind CSS utilities. It is not a full-blown design system like Material UI or an abstract component library like Chakra. Instead it gives you copy-and-paste blueprints for accessible UI parts that remain 100 percent yours after generation. Think of it as a scaffold: it bootstraps predictable, well-tested markup and behaviour, then gets out of your way so you can style or refactor without fighting an API surface.

What Is ShadCN UI?

ShadCN UI is a project maintained by @shadcn that curates Radix primitives into higher-level patterns - buttons, dialogs, dropdowns, tables, tabs, and more. Each component is provided as plain .tsx code plus matching Tailwind classes. There is no package to import at runtime. You run a CLI script once, commit the generated files to your repo, and thereafter treat them like any other local component. Updates are opt-in: rerun the script with the add command, accept the diff in a pull request, customise as you wish.

Key attributes:

Why We Chose ShadCN UI

Core Principles and Design Philosophy

  1. Copy, do not import - you generate source once and version-control it. No vendor lock-in
  2. Radix first - behaviour comes from Radix primitives (Dialog, Popover, ScrollArea) which have battle-tested accessibility
  3. Tailwind for visuals - styling stays declarative and token-driven. There is no CSS-in-JS runtime or Shadow DOM
  4. Plain React - no custom renderers, no wrappers around hooks; what you see is what you get
  5. Opt-in complexity - advanced features (CVA variants, Framer Motion animations) are present but optional

Installing ShadCN UI in a Next.js + Tailwind Stack

The project ships a small CLI you run once during setup. In a fresh repository:

  1. Ensure Tailwind is already configured (our project starter includes it)
  2. Initialise shadcn/ui:
    npx shadcn-ui@latest init
    • The script prompts for your project path (src), components path (components/ui), styling solution (Tailwind CSS), and preferred alias
  3. Generate your first component, for example Button:
    npx shadcn-ui@latest add button

The script drops button.tsx into components/ui. It also updates tailwind.css with any missing plugin or theme extension. Commit the diff - you own these files.

Project Structure and File Conventions

Our should follow similar layout:

app/
    components/
        ui/
            button.tsx
            dialog.tsx
    lib/
        tailwind/
            tailwind.css

Generated files follow these conventions:

Working with Primitives

Buttons

The generated Button uses CVA to expose variant (default | destructive | ghost | link) and size (sm | lg) props. Extend with new variants by editing the cva() call and updating the union type.

Example usage:

<Button variant="destructive" size="lg" asChild>
  <Link href="/danger-zone">Delete account</Link>
</Button>

asChild comes from Radix Slot and lets any element inherit Button behaviour.

Inputs and Forms

shadcn/ui provides input.tsx, label.tsx and textarea.tsx primitives. Combine them with react-hook-form:

const { register } = useForm()
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" {...register('email')} />

The visual style stays consistent because every field shares the same Tailwind classes.

Modals and Dialogs

dialog.tsx wraps @radix-ui/react-dialog with overlay, content, title and description slots. It already traps focus and restores scroll position. Tailwind classes control z-index, backdrop blur and animations.

Navigation Components

menubar, dropdown-menu and navigation-menu cover most header and sidebar patterns. Each file exposes compound sub-components so markup remains readable:

<NavigationMenu>
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Link href="/pricing">Pricing</NavigationMenu.Link>
    </NavigationMenu.Item>
  </NavigationMenu.List>
</NavigationMenu>

Data Display Components

table.tsx, badge.tsx and progress.tsx handle common dashboard widgets. Because styling is Tailwind, you can inject our design tokens (e.g. bg-brand-600) without fighting a theming API.

Theming and Design Tokens

shadcn/ui relies entirely on Tailwind for colours, spacing and radii. Our starter defines tokens in @theme:

@theme {
  --radius-lg: 0.75rem;
  --brand-600: 22 119 255;
}

Component classes reference tokens through bg-brand-600 or rounded-lg. Variants inside CVA also read tokens, ensuring dark-mode and future re-branding remain single-source.

Accessibility Defaults and Customization

Radix primitives guarantee:

You can add extra labels inline:

<Dialog title="Delete account" description="This action is irreversible">

Color contrast remains our responsibility. Use Tailwind's text-brand-foreground tokens and eslint-plugin-jsx-a11y to lint contrast.

Composing and Extending Components

Because each primitive is local code, composition works like ordinary React:

import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cn"; // tailwind-merge helper

export function IconButton(props: ButtonProps & { icon: ReactNode }) {
  const { icon, className, ...rest } = props;

  return (
    <Button className={cn("flex items-center gap-2", className)} {...rest}>
      {icon}
      {props.children}
    </Button>
  );
}

For styling, prefer class-variance-authority (CVA) over ad-hoc template strings. This keeps variants declarative:

const alertVariants = cva("rounded-md border p-4", {
  variants: {
    intent: {
      info: "bg-blue-50 text-blue-700 border-blue-200",
      warn: "bg-yellow-50 text-yellow-700 border-yellow-200",
      error: "bg-red-50 text-red-700 border-red-200",
    },
  },
});

Integrating Animations (Framer Motion)

shadcn/ui does not dictate motion. Wrap primitives with motion():

import { DialogContent } from '@/components/ui/dialog'
import { motion } from 'framer-motion'

const MotionDialog = motion(DialogContent)

<MotionDialog
    initial={{ scale: 0.9, opacity: 0 }}
    animate={{ scale: 1,   opacity: 1 }}
    exit={{    scale: 0.9, opacity: 0 }}
    transition={{ duration: 0.15 }}
>
    {children}
</MotionDialog>

Because styles are static Tailwind classes, no runtime CSS conflicts with Framer Motion transforms.

Performance Considerations

Testing ShadCN Components

Common Pitfalls and How to Avoid Them

Migration Strategy from Other UI Libraries

  1. Audit primitives - list which Chakra or MUI components you actually use
  2. Generate equivalents - run npx shadcn-ui add for each
  3. Create wrappers - keep the old prop API but proxy to shadcn primitives so call sites remain unchanged during migration
  4. Map tokens - translate theme colours and radii into Tailwind config
  5. Remove legacy provider - delete ChakraProvider or ThemeProvider, clean up leftover style resets
  6. Measure - compare bundle size, FCP and Total Blocking Time before deleting the old dependency

Frequently Asked Questions

Further Reading