shadcn ui : End-to-End Component Install Workflow
This skill is the workflow on top of the CLI. It tells you the SEQUENCE of
steps from empty repo to customised, version-controlled component file, and
the SAFE re-run pattern when upstream changes ship. For raw CLI command
surface (every flag, every subcommand), go to shadcn-core-cli. For
registry authoring and resolution rules, go to shadcn-core-registry. For
multi-file page scaffolds, go to shadcn-core-blocks.
Companion skills :
shadcn-core-cli: every CLI command, every flag, components.json schemashadcn-core-architecture: code-distribution paradigm, ownership modelshadcn-core-registry: registry item schema, resolution semanticsshadcn-core-blocks: multi-file page scaffolds (dashboards, login pages)
Quick Reference : The Three Beats
ALWAYS install a component in this order. NEVER skip a beat.
1. init -> pnpm dlx shadcn@latest init
(once per project ; creates components.json + deps)
2. add -> pnpm dlx shadcn@latest add <name>
(per component ; copies source into components/ui/)
3. customise -> edit components/ui/<name>.tsx in place
(variants, props, default styles ; commit to git)
The output of step 2 is OWNED code. After it lands, you are the
maintainer. There is no upgrade path through npm ; the only upgrade
mechanism is re-running add with --diff and merging by hand.
Quick Reference : Add or Customise or Block ?
What are you building ?
├── A single primitive that does not exist in shadcn yet (e.g., a price-tag
│ pill, a domain-specific badge variant)
│ └── COMPOSE shadcn primitives first. NEVER hand-roll a new "Button" or
│ "Dialog". Custom code lives in `components/<name>.tsx` (NOT
│ components/ui/, that path is reserved for CLI-managed files).
│
├── A primitive shadcn already ships (Button, Dialog, Form, Input, ...)
│ └── `pnpm dlx shadcn@latest add <name>` -> then customise in place.
│ NEVER hand-write a re-implementation of a shadcn primitive.
│
├── A whole page or large UI surface (dashboard, login, sidebar shell)
│ └── Use a BLOCK. `pnpm dlx shadcn@latest add sidebar-07` (or browse
│ https://ui.shadcn.com/blocks). See `shadcn-core-blocks` for the
│ full block doctrine.
│
└── A component from a third-party / private design system
└── Configure the `registries` entry in components.json, then
`pnpm dlx shadcn@latest add @myorg/<name>`. See
`shadcn-core-registry` for registry config rules.
ALWAYS prefer add over re-implementing. The CLI resolves runtime
dependencies (radix primitives, cva, sonner, vaul, cmdk, react-day-picker,
input-otp) automatically. Hand-rolling skips that resolution and ships a
file with broken imports.
init : Bootstrap Once Per Project
# Default Next.js + nova preset, no prompts :
pnpm dlx shadcn@latest init -d
# Interactive (recommended for first-time projects) :
pnpm dlx shadcn@latest init
# Vite + React project (after the manual tsconfig setup ; see below) :
pnpm dlx shadcn@latest init -t vite
# Astro project :
pnpm dlx shadcn@latest init -t astro
Interactive init asks for : framework template, base library (radix vs
base), preset, baseColor, cssVariables, monorepo layout, RTL. Outputs
components.json at the project root and installs runtime deps
(tailwind-merge, clsx, class-variance-authority, lucide-react).
ALWAYS commit components.json to git. It is the single source of truth
for where the CLI writes files and which import-alias style your project
uses. Without it the next add invocation will prompt you for the entire
config again.
components.json : the Fields You Touch Most
| Field | Purpose | Common value |
|---|---|---|
style |
Visual language (immutable after init) | "new-york" (recommended) |
tailwind.baseColor |
Neutral palette (immutable) | "neutral" / "zinc" / "stone" |
tailwind.cssVariables |
Semantic tokens (immutable) | true for theming, false for inline utilities |
aliases.utils |
Where cn() lives |
"@/lib/utils" |
aliases.ui |
Where shadcn copies primitives | "@/components/ui" |
aliases.components |
Where custom components live (also blocks) | "@/components" |
registries |
Custom / private registry namespaces | { "@myorg" : "..." } |
Full schema with every field, every default, every mutability rule, and
every Tailwind v3-vs-v4 difference : see shadcn-core-cli "components.json
: Complete Schema".
NEVER change style, baseColor, or cssVariables post-init by hand-
editing the file alone. Those fields are IMMUTABLE in the sense that
already-installed components do NOT regenerate themselves. The supported
"change my mind" path is : init -f (rewrites components.json) THEN
add --overwrite --all (rewrites every installed component file).
Aliases : tsconfig Paths or package.json#imports ?
shadcn rewrites import paths inside the copied files so they match YOUR project's alias scheme. Two schemes are supported as of shadcn@4.7.0.
Approach A : tsconfig paths (traditional, most projects)
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}
// components.json (excerpt)
{
"aliases": {
"utils": "@/lib/utils",
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
ALWAYS use tsconfig paths when : the project is already TypeScript-based,
@/* is already wired, you do not need ESM-native subpath imports, the
tooling chain (Next.js, Vite, Astro, etc.) is the import resolver.
Approach B : package.json#imports (NEW in shadcn@4.7.0)
// package.json (excerpt)
{
"imports": {
"#components/*": "./src/components/*",
"#lib/*": "./src/lib/*",
"#hooks/*": "./src/hooks/*"
}
}
// components.json (excerpt)
{
"aliases": {
"utils": "#lib/utils",
"components": "#components",
"ui": "#components/ui",
"lib": "#lib",
"hooks": "#hooks"
}
}
USE package.json#imports when : the project needs runtime-native private
imports (Node.js #prefix ESM subpaths, no build-tool transform), you are
publishing your components as a package and want consumers to use the same
#-prefixed names, you want a single resolver source-of-truth without
maintaining tsconfig paths in parallel.
NEVER define the same alias in BOTH systems for the same project. The resolution order is implementation-defined and silent. Pick one approach per project at init time.
Source : https://ui.shadcn.com/docs/changelog (May 2026 entry).
Vite-specific : Two tsconfig Files
Vite splits TypeScript config across tsconfig.json AND tsconfig.app.json.
ALWAYS add the alias entry to BOTH files. Missing one of them leaves either
editor IntelliSense broken (resolves at runtime but red squiggle in VS Code)
or runtime broken (compiles in editor but fails at vite-build). Then add
the alias to vite.config.ts resolve.alias as well. The full three-file
setup is shown in examples.md "Vite + React greenfield setup".
add : The Per-Component Command
# Bare name : resolves against @shadcn (default) registry :
pnpm dlx shadcn@latest add button
# Multiple at once :
pnpm dlx shadcn@latest add button card dialog form
# Namespaced (custom registry) :
pnpm dlx shadcn@latest add @myorg/datepicker
# Remote URL :
pnpm dlx shadcn@latest add https://example.com/r/x.json
# Local registry item :
pnpm dlx shadcn@latest add ./registry/datepicker.json
# Block (multi-file page scaffold) :
pnpm dlx shadcn@latest add sidebar-07
What happens during add :
- CLI reads
components.jsonto know aliases and base. - CLI fetches the registry item JSON.
- CLI installs npm runtime dependencies declared by the item (radix primitives, cva, lucide, sonner, vaul, etc.).
- CLI writes file(s) into
aliases.ui(single primitives) oraliases.components(blocks). - CLI rewrites import paths inside the file(s) to match your aliases.
NEVER hand-install @radix-ui/react-*, cmdk, vaul, react-day-picker,
input-otp, sonner packages on your own when adding a shadcn component.
The CLI knows the exact set the component depends on and the exact versions
the registry has verified. Hand-installing skips this and frequently
mismatches versions.
If add encounters a file that already exists at the target path, it
SKIPS by default. Pass --overwrite to force a fresh copy.
Post-Add Fixups (Manual Steps After add)
For most components add is end-to-end. The five cases where you DO need
to touch something by hand :
cnimport sanity check : open the new file, verify the first imports look likeimport { cn } from "@/lib/utils"(or your alias). If the import path is wrong, youraliases.utilsis misconfigured ; fix components.json AND re-runadd <name> --overwrite.Tailwind v4 CSS animation imports : if the component animates (Accordion, Sheet, Dialog, Drawer, Sidebar) and your project is on Tailwind v4, your global CSS file MUST import
tw-animate-cssor the keyframes are missing.initadds this for new projects, but migrating a v3 project to v4 leaves the import absent. Seeshadcn-core-stack"Tailwind v3 vs v4".Server-Component boundary : when
rsc: truein components.json,addomits"use client"from pure-presentation files (Card, Badge, Alert, Skeleton, Separator). Components with state or refs (Dialog, Sheet, Drawer, Popover, Sidebar, Form, Calendar) keep"use client". Verify the directive is present where the component uses hooks.react-day-pickerversion (Calendar only) : Calendar wrapsreact-day-pickerv9 since mid-2025. If your project pinned v8 from an older shadcn install,add calendar --overwriteupdates the file but does NOT bump the dep. Runpnpm up react-day-pickerafter.First add to
aliases.uidirectory : the firstaddinvocation creates thecomponents/ui/directory. ALWAYS commit the directory shell to git after init even if empty, so the alias resolves on fresh clones before any components are added.
Customisation : Edit in Place, Commit, Move On
The owned-code model means customisation IS editing the file in place :
// components/ui/button.tsx (post add, post customise)
const buttonVariants = cva(
"inline-flex items-center justify-center ...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground ...",
destructive: "bg-destructive text-white ...",
outline: "border bg-background ...",
// ADDED for this project :
brand: "bg-brand-500 text-white hover:bg-brand-600 ...",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 ...",
lg: "h-10 px-6 ...",
icon: "size-9",
// ADDED for this project :
xl: "h-12 px-8 text-lg",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
ALWAYS commit customisations to git in a separate commit from the raw
add output. This is the ONLY way to identify your edits when a later
--diff reveals registry changes (the diff base is the registry's
current shape, NOT your last --overwrite). The git log between the
"add" commit and your "customise" commits IS your local-change history.
NEVER add files to components/ui/ that did not come from the CLI. That
path is reserved for CLI-managed files. Custom compositions live in
components/<name>.tsx (under aliases.components).
Update Workflow : Diff Before Overwrite
Components drift from upstream over time : new variants ship, bugs get
fixed, accessibility wiring improves. Refusing to ever re-run the CLI is
the slow-rot anti-pattern : your local Button is six months behind, the
new cursor-pointer flag, the new data-icon slot, the new size-icon-sm,
the new aria-attribute, never arrive.
The safe sync workflow :
# 1. Preview ONE component :
pnpm dlx shadcn@latest add button --diff
# 2. Preview ALL components (heavy output, capture to file) :
pnpm dlx shadcn@latest add -a --diff > shadcn-diff.txt
# 3. Decide per component : accept rewrite, or merge by hand.
# If you accept rewrite :
pnpm dlx shadcn@latest add button --overwrite
# Then re-apply your custom variants from the previous commit (git diff
# against the pre-overwrite commit shows exactly what to port).
# 4. Commit BEFORE running --overwrite. Always.
git add . && git commit -m "Snapshot before shadcn sync"
pnpm dlx shadcn@latest add button card dialog --overwrite
ALWAYS run --diff before --overwrite on any file you have edited.
NEVER assume re-running add is safe : when the file exists, add skips
by default, --overwrite replaces wholesale. There is no automatic merge.
NEVER skip the pre-overwrite commit. The CLI does not snapshot the previous content ; recovery without a git commit is manual.
There is no shadcn diff SUBCOMMAND despite some blog posts claiming
otherwise. --diff is a FLAG on add. There is no shadcn update either ;
the diff-then-overwrite pair IS the update workflow.
Block-Add : Forward to shadcn-core-blocks
# Login pages :
pnpm dlx shadcn@latest add login-01
pnpm dlx shadcn@latest add login-03
# Sidebars :
pnpm dlx shadcn@latest add sidebar-07
# Dashboards :
pnpm dlx shadcn@latest add dashboard-01
Blocks are added through the SAME add command, but write multiple files
to aliases.components (NOT aliases.ui). Block file ownership semantics
match component ownership : edit in place, commit, re-run with --diff
when you want to pull upstream improvements. Full block doctrine, gallery
landscape, style enum guidance, and the block-vs-component decision tree
live in shadcn-core-blocks.
Custom Registry Add : the --registry Form
For a third-party or internal design system, configure the registries
field in components.json :
{
"registries": {
"@myorg": "https://registry.myorg.com/{name}.json",
"@private": {
"url": "https://private.registry.io/{name}.json",
"headers": { "Authorization": "Bearer ${REGISTRY_TOKEN}" },
"params": { "version": "stable" }
}
}
}
Then add by namespace :
pnpm dlx shadcn@latest add @myorg/datepicker
pnpm dlx shadcn@latest add @private/billing-form
Resolution rules (verified at https://ui.shadcn.com/docs/registry) :
{name}placeholder REQUIRED in the URL ; the CLI substitutes the item name at fetch time.${ENV_VAR}interpolation supported in headers and params ; the CLI reads fromprocess.envat invocation time.- Custom registries appear as
@namespaceprefixes. - The default
@shadcnis implicit ; declare it only to override the URL.
NEVER hard-code secrets in URL or headers. ALWAYS interpolate via
${VAR_NAME} and store the secret in .env / CI secret store / OS keychain.
Full registry item schema, authoring rules, and the build command for
publishers : see shadcn-core-registry.
Reference Files
references/methods.md: full init prompts, everyaddflag, the diff-and-overwrite workflow commands, the aliases comparison tablereferences/examples.md: Next.js greenfield setup, Vite greenfield setup with both tsconfig files, add Button + customise variant, add Form + integrate with existing context, update workflow with diff, package.json#imports setup, custom registry addreferences/anti-patterns.md: init-skip trap, missing-diff trap, alias mismatch, hand-installed radix versions, never-re-running-the-CLI
Verified Sources
- https://ui.shadcn.com/docs/cli (CLI command surface, flags ; verified 2026-05-19)
- https://ui.shadcn.com/docs/installation (per-framework setup index ; verified 2026-05-19)
- https://ui.shadcn.com/docs/installation/vite (Vite two-tsconfig requirement ; verified 2026-05-19)
- https://ui.shadcn.com/docs/components-json (schema ; verified 2026-05-19)
- https://ui.shadcn.com/docs/registry (registry resolution ; verified 2026-05-19)
- https://ui.shadcn.com/docs/changelog (4.7.0 package.json#imports entry ; verified 2026-05-19)