shadcn ui : Core Architecture
Foundational mental model for shadcn ui. Misreading shadcn as "another component library" produces every common failure documented in references/anti-patterns.md. ALWAYS internalise this skill before any other.
Quick Reference
shadcn in one sentence
ALWAYS describe shadcn as : a code-distribution platform that copies React component source files into the consumer project via a CLI, so the consumer owns the source outright and customises it freely. It is NOT a runtime dependency, NOT an npm package, NOT a traditional component library.
The official tagline (verified verbatim at https://ui.shadcn.com/docs) :
"This is not a component library. It is how you build your component library."
Four invariants
- ALWAYS install components with
pnpm dlx shadcn@latest add <component>(ornpx,bunx,yarn dlx). NEVERnpm install shadcn-ui; no such runtime package exists. - ALWAYS import components from the LOCAL alias (
@/components/ui/button) after they are added. NEVER fromshadcn-ui,@shadcn/ui, or any external module path. - ALWAYS preview a re-install via
shadcn add <component> --diffbefore running with--overwrite. NEVER overwrite a customised component without first inspecting the diff. - ALWAYS treat the local component file as YOUR source code. NEVER expect automatic upgrades via
npm update; upgrades come from re-runningshadcn addplus a manual merge.
The 5 official pillars (verbatim at https://ui.shadcn.com/docs)
- Open Code : "The top layer of your component code is open for modification."
- Composition : "Every component uses a common, composable interface, making them predictable."
- Distribution : "A flat-file schema and command-line tool make it easy to distribute components."
- Beautiful Defaults : "Carefully chosen default styles, so you get great design out-of-the-box."
- AI-Ready : "Open code for LLMs to read, understand, and improve."
ALWAYS quote these pillars when justifying an architectural choice (file location, customisation strategy, registry design). NEVER paraphrase into a different list.
Stack composition (runtime dependencies)
When shadcn add button runs, the CLI copies button.tsx into components/ui/. The file imports from these runtime packages, all installed as normal npm dependencies in the consumer project :
| Layer | Package | Role |
|---|---|---|
| Headless primitive | radix-ui (unified since Feb 2026) or per-primitive @radix-ui/react-* |
Accessibility + state for Dialog, Popover, Select, etc. |
| Variant API | class-variance-authority (cva) |
Type-safe variant + size composition |
| Class conflict resolver | tailwind-merge |
Deduplicates conflicting Tailwind utilities |
| Class joiner | clsx |
Conditional class composition |
| Styling | tailwindcss (v3.4 or v4) |
Utility classes + CSS variables |
| Icons | lucide-react (default) |
Default icon set; swap via shadcn migrate icons |
These ARE npm dependencies. The shadcn CLI is also an npm package (shadcn, currently 4.7.0), but it is invoked via dlx/npx and runs at install time, never at runtime.
Decision Tree 1 : Is shadcn the right choice for this project?
Q1. Will the project use React (Next.js / Vite / Remix / React Router v7 / Astro / TanStack Start / Laravel + Inertia)?
no → shadcn is React-only ; choose a framework-native option
yes → Q2
Q2. Does the project use, or want to use, Tailwind CSS (v3.4 or v4)?
no → shadcn is Tailwind-coupled ; choose MUI / Chakra / Mantine / Ant Design instead
yes → Q3
Q3. Does the team want to OWN and CUSTOMISE component source?
no → choose MUI / Chakra / Mantine ; runtime libraries cost less in time-to-screen
yes → Q4
Q4. Is the team willing to accept manual merges when upgrading components?
no → choose a versioned library
yes → ALWAYS recommend shadcn
Decision Tree 2 : Library model vs shadcn model
┌─ Traditional library (MUI / Chakra / Mantine / Ant)
│ • npm install once, import from package path
│ • Maintainer owns API, breaking changes propagate via semver
│ • Customisation via theme prop + style overrides
│ • Tree-shake at bundle time
│ • Wins when : speed-to-screen > customisation depth
│
Component strategies ───┤
│
└─ shadcn (copy-not-install)
• CLI copies source into project
• Consumer owns source, customises freely
• Upgrades are explicit re-runs + manual merge
• Underlying primitives (Radix) are still npm deps
• Wins when : customisation depth > upgrade ergonomics,
and AI code generation matters
ALWAYS choose shadcn when AI-assisted development is a primary workflow : because the source is local, Claude can read, modify, and reason over it without indirection through a node_modules black box. NEVER choose shadcn for a team that explicitly prefers maintainer-managed API evolution.
Decision Tree 3 : Adding a component to a project
Q1. Has the project been initialised with shadcn (does `components.json` exist)?
no → ALWAYS run `pnpm dlx shadcn@latest init` FIRST and configure
style / baseColor / cssVariables / aliases (these are immutable after init)
yes → Q2
Q2. Does the local `components/ui/<component>.tsx` already exist?
no → `pnpm dlx shadcn@latest add <component>` and commit
yes → Q3 (re-install scenario)
Q3. Has the local file been customised?
no → `pnpm dlx shadcn@latest add <component> --overwrite` is safe
yes → ALWAYS run `--diff` first, review changes, choose : skip,
overwrite-and-redo-customisations, or hand-merge
Decision Tree 4 : Versioning expectations
Q1. Are you tracking shadcn's "evergreen" model (current canary, ~2026)?
yes → Q2
no → pin specific versions of underlying deps (radix-ui, cva, tailwind-merge,
lucide-react) in package.json ; re-run `shadcn add` only on demand
and merge manually
Q2. Did a recent re-install break something?
yes → check : (a) Calendar broke on react-day-picker v9 (issue #4366),
(b) Combobox/Command broke on cmdk version drift (issues #2944, #2980, #3051),
(c) Tailwind v4 changed color tokens from HSL to oklch.
See [shadcn-errors-react-day-picker-v9] and
[shadcn-errors-cmdk-version-drift] and
[shadcn-errors-tailwind-v3-v4-migration].
no → ALWAYS lock the working state in git ; the source IS your version control
Patterns
Pattern 1 : Copy-not-install paradigm
The shadcn CLI replaces the conventional install/import flow :
Traditional library (MUI example) :
npm install @mui/material
import { Button } from '@mui/material'
shadcn :
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button
import { Button } from '@/components/ui/button' // LOCAL alias to project source
After add, the file components/ui/button.tsx lives in the consumer's repository. The consumer can :
- Add a new variant to the
cva()definition. - Change default classes.
- Remove unused props.
- Rename the component.
- Add framework-specific wiring (next/link, react-router NavLink).
The shadcn project does NOT track this file. No automated upgrade exists. The consumer's source repository IS the version system.
Pattern 2 : The ownership doctrine
Shadcn's design treats every copied component as "your code". This produces five concrete obligations :
- ALWAYS commit
components/ui/*.tsxto source control as first-class application code, not as vendored dependencies. - ALWAYS lint and type-check those files together with the rest of the codebase.
- ALWAYS expect future shadcn updates to require manual reconciliation; there is no semver-driven rollout.
- ALWAYS treat removed components as deletable; if the project never uses
Carousel, deletecomponents/ui/carousel.tsx. - ALWAYS prefer extending the file in place over wrapping it. Wrapper components defeat the "Open Code" pillar.
Pattern 3 : The 5 pillars and what they imply
| Pillar | Verbatim wording | Practical implication |
|---|---|---|
| Open Code | "The top layer of your component code is open for modification." | ALWAYS feel free to edit components/ui/*.tsx. NEVER fear forking a primitive. |
| Composition | "Every component uses a common, composable interface, making them predictable." | ALWAYS expect Trigger / Content / Header / Footer / Title / Description / Close subcomponents (where applicable). ALWAYS expect asChild for Slot composition. |
| Distribution | "A flat-file schema and command-line tool make it easy to distribute components." | ALWAYS distribute custom components via your own registry (see shadcn-core-registry). NEVER publish them as an npm component library if reuse is internal-only. |
| Beautiful Defaults | "Carefully chosen default styles, so you get great design out-of-the-box." | ALWAYS start from defaults; customise only after seeing them in context. NEVER pre-emptively override styles you have not yet observed. |
| AI-Ready | "Open code for LLMs to read, understand, and improve." | ALWAYS leverage AI assistants on the local source. NEVER hide components behind opaque wrappers that an LLM cannot reason over. |
Pattern 4 : Registry resolution
Shadcn separates the CLI from the registry of components it installs. The default registry is @shadcn (https://ui.shadcn.com/registry). The CLI resolves a component reference like this :
| Input | Resolves to |
|---|---|
pnpm dlx shadcn@latest add button |
@shadcn/button (default registry) |
pnpm dlx shadcn@latest add @acme/datepicker |
URL configured for @acme namespace in components.json |
pnpm dlx shadcn@latest add https://internal.example.com/registry/button.json |
Direct registry URL |
pnpm dlx shadcn@latest add login-01 |
Block (multi-file scaffold) from the default registry |
ALWAYS configure custom registries in components.json under the registries key. NEVER hard-code URLs in shell aliases or scripts. See references/examples.md for the full schema.
Pattern 5 : Evergreen-2026 versioning
The shadcn CLI is semver-versioned (shadcn@4.7.0 as of 2026-05). The COMPONENTS are evergreen : each shadcn add returns the current canonical source. Implications :
- ALWAYS pin a known-good commit of
components/ui/*.tsxin your repo. That is your version. - ALWAYS lock underlying runtime dependencies (
radix-ui,cva,tailwind-merge,lucide-react) inpackage.jsonso apnpm installdoes not silently regress. - ALWAYS verify breaking changes via https://ui.shadcn.com/docs/changelog before any wholesale re-install.
Pattern 6 : Deliberate tradeoffs versus library model
| Concern | Traditional library | shadcn |
|---|---|---|
| Time to first screen | seconds (one npm install) |
minutes (init + add per component) |
| Customisation depth | medium (theme + variants) | unlimited (source ownership) |
| Upgrade ergonomics | automatic (semver) | manual (re-run + merge) |
| AI-assistant compatibility | medium (must reason via docs / types) | high (reads local source) |
| Tree-shaking | bundle-time | irrelevant : only-used files exist |
| Wrapper components | common | discouraged (Open Code pillar) |
| Source observability | low (node_modules) | high (in project) |
| Maintainer API control | high | none (you own the code) |
| Lock-in | dependency-shaped | none (forking is the default state) |
ALWAYS pick shadcn when source observability, AI-assistant compatibility, and customisation depth dominate. NEVER pick shadcn when time-to-first-screen and upgrade ergonomics dominate.
Anti-patterns (summary; full catalogue in references/anti-patterns.md)
NEVER do these :
- NEVER run
npm install shadcn-ui: no such runtime package exists; installation is viadlx/npxinvocation of theshadcnCLI which then copies source. - NEVER import from
shadcn-ui,@shadcn/ui, or any external package : all components live under the local alias (default@/components/ui/...). - NEVER expect
npm updateto upgrade components : there is no library to update; components are local source. Upgrade by re-runningshadcn addwith--overwriteor--diff. - NEVER run
shadcn add <component> --overwriteon a customised file without first running--diff: the overwrite is irreversible; customisations are lost. - NEVER paraphrase the 5 pillars : they have official verbatim wording at https://ui.shadcn.com/docs.
- NEVER wrap a shadcn component in your own component just to add props : edit the source directly (Open Code pillar). Wrapping defeats the model.
Reference Links
- references/methods.md : note on where API methods live (CLI skill, registry skill)
- references/examples.md :
components.jsonsample, CLIaddinvocations, registry override syntax - references/anti-patterns.md : four canonical anti-patterns with WHY each fails and the fix
Cross-references
- shadcn-core-cli : full CLI surface (init / add / view / search / apply / preset / build / docs / info / migrate)
- shadcn-core-stack : detailed Radix + cva + tailwind-merge composition map
- shadcn-core-theming : CSS custom properties, HSL vs oklch
- shadcn-core-registry :
components.jsonregistries field, custom registry hosting - shadcn-core-blocks : block distribution surface, style enum
- shadcn-errors-cli-sync-mismatch : recovery from accidental overwrite
Sources
All claims in this skill trace to URLs in SOURCES.md :
- https://ui.shadcn.com/docs (the 5 pillars and "not a component library" tagline, verbatim)
- https://ui.shadcn.com/docs/cli (CLI invocation syntax and verbs)
- https://ui.shadcn.com/docs/components-json (schema for
components.json) - https://ui.shadcn.com/docs/registry (registry resolution model)
- https://ui.shadcn.com/docs/changelog (versioning history)
- https://github.com/shadcn-ui/ui (source confirmation)
- https://github.com/shadcn-ui/ui/releases (CLI semver history)
Verified 2026-05-19.