Skip to content
C Codeloom
Tailwind

Tailwind with shadcn/ui: An Overview

A practical overview of how Tailwind CSS and shadcn/ui work together to give you a component library you can actually own and customize.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How shadcn/ui differs from traditional component libraries
  • How Tailwind powers the styling layer
  • How to add and customize components
  • How theming with CSS variables works
  • When shadcn/ui is the right choice

Prerequisites

  • Comfortable with HTML and Tailwind utility classes

If you have used component libraries before, you know the trade-off: you get speed up front but pay later when you need to customize anything. shadcn/ui flips that trade-off by giving you the source code of every component and styling it with Tailwind. This post walks through the mental model and how the pieces fit together.

What it is and why it matters

shadcn/ui is not an npm package you install once and import from. It is a CLI that copies component source files directly into your project. You own them. They live next to your code, they get committed to your repo, and you change them like any other file. Tailwind handles the visual layer through utility classes and CSS variables for theme tokens.

This matters because most teams eventually need to customize a button, a dialog, or a dropdown beyond what the original author imagined. With a normal library you fight the abstraction. With shadcn/ui you just edit the file.

Mental model

Think of it as three layers stacked on top of each other. Radix UI primitives provide accessible, unstyled behavior at the bottom. Tailwind classes provide the visual styling in the middle. CSS variables in your global stylesheet hold the theme tokens at the top, so a single change cascades through every component.


+----------------------------------------+
|  Theme tokens (CSS variables)          |
|  --background, --foreground, --primary |
+----------------------------------------+
|  Tailwind utility classes              |
|  bg-background text-foreground rounded |
+----------------------------------------+
|  Radix primitives (behavior, a11y)     |
|  Dialog.Root, Dialog.Trigger, ...      |
+----------------------------------------+
The three layers behind a shadcn/ui component

When you flip from light to dark mode, only the CSS variables change. Tailwind classes like bg-background keep working because they reference the variables, not hardcoded colors.

Hands-on example

You start by initializing shadcn/ui in a project that already has Tailwind configured. The CLI asks a few questions and writes a components.json file plus a globals.css with theme variables.

npx shadcn@latest init
npx shadcn@latest add button dialog

After running those commands you will find new files in your components/ui directory. Open button.tsx and you will see a regular React component with cva-based variant handling and Tailwind classes. Nothing hidden, nothing magic.

import { Button } from "@/components/ui/button"

export function SaveBar() {
  return (
    <div className="flex gap-2 p-4 bg-background border-t">
      <Button variant="outline">Cancel</Button>
      <Button>Save</Button>
    </div>
  )
}

If your design calls for a softer corner radius or a different focus ring, you open button.tsx and change the classes. No PRs to a third-party library, no overrides fighting specificity wars, no waiting for a maintainer to merge your change.


CLI add  -->  file copied to /components/ui
                        |
                        v
            you edit Tailwind classes
                        |
                        v
            you commit to your repo
                        |
                        v
         future updates are manual, on your terms
The lifecycle of a shadcn/ui component in your repo

Common pitfalls

The first pitfall is treating shadcn/ui like a versioned dependency. There is no upgrade command for components you have already added. If the upstream improves a component you have to manually pull the new version and merge with your changes. This is a feature, not a bug, but it surprises people.

The second is forgetting to install Radix peer dependencies. The CLI usually handles this but if you copy files manually you can end up with missing packages and confusing runtime errors.

The third is overriding the CSS variables in the wrong place. The :root selector and the .dark selector both need their full token set. Drop a variable and a component somewhere will render with a transparent background.

A subtler one is using arbitrary Tailwind values everywhere instead of leaning on the theme. The whole point of the variable system is consistency. If half your components use bg-primary and the other half use bg-[#3b82f6], your design system has already fractured.

Best practices

Keep the generated components mostly stock until you actually need to change them. Resist the urge to refactor on day one. When you do customize, leave a short comment explaining what changed and why, so the next person knows the file diverged from upstream.

Centralize tokens. If a color, radius, or spacing value appears in more than two components, promote it to a CSS variable and reference it from your Tailwind config. That way a redesign is a token change, not a search-and-replace.

Use the cva variants pattern that ships with the components. It is small, predictable, and TypeScript-friendly. Adding a new variant is usually three lines.

Finally, lean on the Radix layer for accessibility. Keyboard handling, focus management, and ARIA roles are already done. Do not strip those wrappers to save a few bytes.

Wrap-up

shadcn/ui is less a library and more a workflow: copy, own, edit. Tailwind gives you the styling vocabulary, Radix gives you the behavior, and CSS variables give you the theme. The result is a component system that scales with your team because nothing is hidden behind an import. Once you internalize the model, the speed of building polished UI goes up sharply, and so does your control over how it looks years from now.