Shadcn CLI v2 + Tailwind v4: The Component Workflow That Killed My UI Library

TL;DR — Shadcn CLI v2 turns component sharing into a copy-paste workflow with private registries and three-way diff updates. On Tailwind v4's CSS-native theming, it's the smallest tooling surface for shared React components — and the reason to delete your hand-rolled UI library.

I spent two years maintaining a hand-rolled component library. Storybook, semantic versioning, changesets, the whole ceremony. Last month I deleted it and moved every consumer to shadcn ui. Nobody complained. The design tokens are cleaner, the bundle is smaller, and I stopped getting Slack pings about breaking changes in v3.7.2.

This is the shadcn CLI v2 workflow on Tailwind v4 that made that possible — theming, private registry hosting, and the update strategy that actually preserves your local edits.

react developer
react developer

Why owning the source beats npm-installing a design system

Owning the source means your components live in your repo as plain React + Tailwind files, not behind a package version. shadcn ui 2026 doubled down on this: the CLI copies files, generates types against your local Tailwind theme, and treats updates as diffs you review — not black-box breaking changes.

The pain of an npm-installed UI kit shows up on day 400, not day 1. You want to tweak a Dialog overlay's blur. You can't. You fork it. Now you maintain a fork. You upgrade — merge conflicts. Owning the source skips that entire class of problem. Your components/ui/dialog.tsx is yours. Editing it is a normal PR.

  1. Scaffold a React + Vite + TypeScript project and install Tailwind v4 with the Vite plugin.
  2. Run npx shadcn@latest init to set up components.json, CSS variables, and the base theme.
  3. Add components with npx shadcn@latest add button dialog form — files land in src/components/ui.
  4. Define your design tokens in @theme inside src/index.css (v4's CSS-native config).
  5. Host a private registry — a JSON file behind auth — for internal, shared components.
  6. Run npx shadcn@latest diff before every upstream sync to review upstream changes against your local edits.
  7. Commit component files to git and treat them like normal source, not vendor code.

How do you set up shadcn CLI v2 with Tailwind v4?

Answer: install Tailwind v4 via @tailwindcss/vite, then run npx shadcn@latest init — the CLI now detects v4 automatically and skips the old tailwind.config.ts generation step.

Concrete commands, assuming a fresh Vite scaffold:

pnpm create vite@latest my-app --template react-ts
cd my-app
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node

Wire up vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: { alias: { '@': path.resolve(__dirname, './src') } },
})

Then src/index.css:

@import "tailwindcss";

@theme {
  --color-background: hsl(0 0% 100%);
  --color-foreground: hsl(240 10% 3.9%);
  --color-primary: hsl(240 5.9% 10%);
  --radius: 0.5rem;
}

Now npx shadcn@latest init. When it asks about your CSS variables file, point it at src/index.css. It writes components.json and you're done. The docs don't emphasise this enough: on v4 you skip tailwind.config entirely. Everything lives in CSS.

What changed in shadcn CLI v2?

Answer: the v2 CLI adds first-class registry support, per-component dependency resolution, and a diff command that shows upstream changes without clobbering your local edits.

The three flags I actually use:

  • shadcn add <url> — pulls a component from any HTTP(S) URL that returns registry-shaped JSON. This is how private registries work.
  • shadcn diff <component> — three-way diff between your local file, the version you originally added, and the latest upstream. Killer feature.
  • shadcn build — compiles a directory of your own components into registry JSON that other repos can consume.

The diff command is what finally made shadcn viable at team scale. Before v2, updating meant re-running add and hoping. Now it's the same workflow as reviewing a dependency bump.

vscode terminal
vscode terminal

How do you host a private shadcn registry?

Answer: a shadcn registry is a static JSON endpoint. Host it on any static file provider — a private S3 bucket, a GitHub Pages repo, or a Next.js app on Vercel with an API route that gates access by header.

The registry format is a JSON file per component listing files, dependencies, and Tailwind tokens. Minimum viable version:

{
  "name": "data-table",
  "type": "registry:component",
  "dependencies": ["@tanstack/react-table"],
  "registryDependencies": ["button", "input"],
  "files": [
    { "path": "data-table.tsx", "content": "...", "type": "registry:component" }
  ]
}

Consume it from any project:

npx shadcn@latest add https://registry.internal.acme.dev/data-table.json

The CLI resolves registryDependencies against your public shadcn setup automatically, so internal components can build on button and input without vendoring them again. This is how you replace an internal UI library: your team's registry sits alongside the public one, and consumers cherry-pick.

How do you update components without breaking local diffs?

Answer: run shadcn diff before shadcn add --overwrite. The diff shows exactly what changed upstream, so you apply upstream changes as manual edits rather than losing your customisations to a blind overwrite.

My actual workflow, weekly:

npx shadcn@latest diff
# review changes per component
npx shadcn@latest diff button --apply
# or manually patch the bits you want

The --apply flag does a three-way merge and leaves conflict markers where your edits collide with upstream. Same mental model as a git rebase. Anyone who's survived a Stack Overflow rabbit hole trying to salvage a mangled Radix upgrade will appreciate this workflow. If you've followed our Tailwind v4 upgrade guide, this slots straight in on top.

Should you migrate an existing UI library to shadcn?

Answer: yes if your library is <5 developers maintaining <30 components and your consumers all live in the same monorepo or org. No if you're shipping a public design system used by external teams — that still wants versioned npm packages.

My migration path was boring on purpose:

  • Week 1 — stood up a private registry with our 12 most-used components (Button, Input, Dialog, etc.) mapped to our design tokens.
  • Week 2 — one consumer app ran shadcn add against every internal registry URL, deleted the old @acme/ui import, and shipped.
  • Week 3 — remaining apps migrated. Deprecated the npm package.

Total: about 40 hours of work across three sprints. What we got back: no more version negotiation across apps, no more "can you cut a patch release for this one prop?", designers can PR directly against component files. If you want to level up your test story around vendored components, wire in Playwright component testing — real DOM in CI catches things unit tests miss.

The verdict

Ship shadcn ui + Tailwind v4 for any React project where you control the consumers. The shadcn CLI v2 workflow — init, add, diff, private registry — is the smallest amount of tooling ceremony I've seen for shared components. Own the source. Skip the semver theatre.

FAQs

Is shadcn ui a component library?

No — it's a CLI that copies component source files into your project. There's no npm package to install for the components themselves. You get the source, you own it, and you edit it like any other file in your repo.

Does shadcn work with Tailwind v4?

Yes. The v2 CLI detects Tailwind v4 automatically and generates components that use the CSS-native @theme block instead of the old tailwind.config.ts. No PostCSS setup needed.

How do I share shadcn components across multiple projects?

Publish a private registry — a JSON endpoint that describes your components and their dependencies. Any project can then run npx shadcn add <your-registry-url> to pull them in, exactly like the public registry.

Can I update shadcn components after I've customised them?

Yes, that's what the shadcn diff command exists for. It performs a three-way merge between your local file, the original, and the latest upstream, leaving conflict markers where your customisations disagree with upstream changes.

Is shadcn better than Material UI or Mantine?

Different tool for a different problem. MUI and Mantine give you a versioned npm package with theming APIs — good for teams that want zero maintenance surface. shadcn gives you source ownership — better when you need custom design tokens, small bundle sizes, or want to avoid framework lock-in from a runtime dependency.

H
Hiten

Senior Frontend Engineer & Architect. 15+ years building fast, accessible web platforms. More at hiten.dev