react-shadcn-ui
Grounded stack guidance: pull idiomatic Do/Don't + docs URLs via
./scripts-run <skills-root>/corpus-grounding/scripts/ground search --manifest <skills-root>/design-intelligence/data/manifest.json --stack shadcn "<topic>" (also --stack react, --stack nextjs). See
design-intelligence.
Component installer — scripts/shadcn_add.ts (gated, assisted)
Bundled installer (Apache-2.0-derived, see header + design-intelligence/ATTRIBUTION.md)
wraps npx shadcn@latest add <components> — the only subprocess+network
surface in the adopted suite. Per runtime-safety + the execution
block above:
- Propose, never silent-run — always show the exact
npx command +
component list first (use --dry-run); the user confirms before any
live run.
- Missing tool → per
missing-tool-handling: if npx/Node is
absent, STOP and ask (install vs. manual component copy) — never
silently work around.
- Verify after run — confirm the component landed
(
components/ui/<name>.tsx exists, components.json unchanged or
sanely updated) before reporting success.
Compatibility
- Tested against:
shadcn@4, Tailwind CSS 4.x, React 19, primitives from
@base-ui/react@1, components.json style base-nova — every major read out
of tests/fixtures/stack/shadcn-current/package.json, the verbatim output of
npx shadcn@latest init -d --template vite on 2026-08-24. No version on this
line may be stated from prose; move it only by re-running that scaffold and
re-committing the fixture.
- Tailwind
3.x and Radix primitives stay supported through the v3 branch
under § Gotcha — the CLI moved on, existing projects did not.
- The audit step (
directives/ui/audit.ts) reads the line above and
compares it with state.ui_audit.shadcn_inventory.version; a major
mismatch triggers a soft halt before this skill runs.
When to use
Use when state.stack.frontend == "react-shadcn" and directives/ui/apply.ts,
review.ts, or polish.ts dispatches to this skill, or when a React project
clearly uses shadcn/ui (presence of components.json, a @base-ui/react or
@radix-ui/* dependency, a components/ui/ folder of generated primitives).
Do NOT use when:
- Project is Blade + Livewire + Flux (use
flux / livewire / blade-ui).
- Project is Vue (use the Vue stack skills).
- Plain React without shadcn/ui — fall back to manual composition; this skill
assumes the primitive set exists.
Gotcha
- shadcn/ui is not an npm package. Primitives are copied into
components/ui/ and edited in-place. Do not npm install shadcn-ui.
Run npx shadcn@latest add <primitive> to scaffold; then edit.
- Major-version drift between this skill's
## Compatibility line and
the project's installed primitives is a real risk. The audit step
writes state.ui_audit.shadcn_inventory with the detected version —
when it diverges by a major, audit emits a soft halt before this
skill runs.
- shadcn/ui composes a primitive vendor —
@base-ui/react in a CLI-4 scaffold,
@radix-ui/* in older projects. Accessibility is built in either way, but
only when the wrapper components are used correctly (asChild,
<DialogTrigger> instead of a bare <button>).
- Tailwind tokens: branch on the
css axis, never guess. detect_stack()
emits axes.css as tailwind-v4 or tailwind-v3
(work_engine/stack/detect.ts:521-524); existing-ui-audit reads tokens on
the same key.
tailwind-v4 — no config file exists. Tokens live in the @theme block
of the entry CSS named by components.json → tailwind.css, and that file's
"tailwind": {"config": ""} — an empty config string — IS the v4 marker.
Never write tailwind.config.{js,ts} in a v4 project.
tailwind-v3 — tokens come from tailwind.config.{js,ts}
(theme.extend.colors).
- Either branch — CSS custom properties on
:root and .dark
(--background, --foreground, --primary, --ring, …). Audit writes them
into state.ui_audit.design_tokens. Use those tokens; never hardcode values.
- Dark mode is class-based (
<html class="dark">). Every color must come
from bg-background, text-foreground, etc. — never raw bg-white.
- Every interactive primitive must declare a focus-visible state via
focus-visible:ring-2 focus-visible:ring-ring; that comes for free with
the generated primitives but is easy to remove during a refactor.
- Anti-AI-slop: shadcn-default look. The out-of-the-box shadcn
theme +
Inter-as-system-fallback + neutral grays reads as
template across projects (catalog T7/T8 + C5). Unless
state.ui_audit.design_tokens pins the neutral palette as the
project's identity, the polish step should match typography and color
tokens to the design brief's aesthetic: line (from fe-design
aesthetic-direction). Theme/font drift within a single audited project
breaks consistency — variation lives between projects, not between
components in the same surface.
- Anti-AI-slop catalog + linter. Pull
docs/guidelines/design-antipatterns.md
before the polish step (Visual V1–V8, Layout L1–L10 are the React-component
slop tells); the objective quality floors (WCAG contrast, focus-visible,
reduced-motion) are validated via accessibility-auditor's checklist —
cite its verdict rather than eyeballing.
Covered primitives
This skill is validated against the following shadcn primitives at the
declared version:
- Form / inputs:
Button, Input, Textarea, Checkbox,
RadioGroup, Select, Switch, Label, Form (react-hook-form
wrapper + zodResolver).
- Overlay:
Dialog, Sheet, Popover, Tooltip, DropdownMenu,
AlertDialog.
- Layout:
Card, Separator, Tabs, Accordion, ScrollArea.
- Data display:
Table (with @tanstack/react-table), Badge,
Avatar, Skeleton, Progress.
- Feedback:
Toast (sonner), Alert.
Not covered — fall back to manual composition
- Marketing-only components (Hero, Pricing, Features) — outside shadcn/ui.
Calendar / DatePicker — composition skill required, not generated.
Combobox — built from Command + Popover; case-by-case.
- Streaming / partial-prerender boundaries — use the project's framework
patterns (Next.js / Remix), not shadcn/ui.
Registry & MCP awareness (opt-in)
The default path is the bundled scripts/shadcn_add.ts CLI wrapper + reading
components.json — it works on most shadcn projects and stays the default.
The modern registry model is an opt-in enhancement; do not add round-trips
to every component op. Full JSON-schema + namespace detail is lazy-loaded from
references/registry.md — read it only on this path,
not on the vanilla add.
shadcn info --json handshake — run it as the grounding step when the
project declares custom/namespaced registries in components.json, OR when
theme-alignment is in scope. It returns framework, aliases, installed
components, icon lib, and base settings. Do NOT make it a forced first action
on every add (over-gating; low ROI on vanilla projects).
- Precedence vs our own audit: prefer a live
shadcn info --json when
available; fall back to state.ui_audit.shadcn_inventory (from
existing-ui-audit) when the CLI/MCP is not reachable. They answer the same
question (project context) — the live read wins.
Namespaced installs — @ns/item resolves via the registries map to a
registry-item.json URL (see the reference). Run view @ns/item to inspect
the JSON before add. Honour registryDependencies (install the graph,
including version-pinned GitHub refs like acme/ui/button#v1.2.0); keep
propose-never-silent-run + --dry-run.
Token-aware scaffolding — when a registry-item.json carries cssVars
(OKLCH, light/dark/theme), align additions to the project's existing tokens
(from info --json / components.json / state.ui_audit.design_tokens) —
never inject the default shadcn neutral theme (it is a flagged anti-slop
tell: default theme + Inter fallback + neutral grays).
MCP path (opt-in) — the shadcn MCP server exposes browse / search-across-
registries / install-with-natural-language over MCP; configure per the
mcp skill. It is an alternative to the CLI, never a hard
dependency. Decision note: CLI path = default + universal; MCP path = opt-in
when the user has it configured; registry-JSON literacy underpins both.
Publish a registry — the library as a source others install from
The sections above consume a registry. This one publishes one: a component library in
this repository can expose its own components the same way, so a consumer installs them with
the tool they already use instead of copying files.
- Author
registry.json at the library root — the index — and one
registry-item.json per exposed component, each naming its files, its
registryDependencies, and its cssVars when it carries token requirements.
- Build to
public/r with the registry build command. The installer gate applies
here exactly as it does to add: propose it, run --dry-run first, and let the user
confirm. A build writes files, so nothing about it being "our own" registry lifts the
gate.
- The consumer adds a
registries map entry pointing at the published index; from then
on @ns/item resolves through it.
Two registry item types are FORBIDDEN, deprecated in v4: registry:build and
registry:mcp. Use registry:base and registry:font instead. Writing either
deprecated type produces an item the current CLI does not understand, and the failure surfaces
at the consumer rather than at authoring time.
dependencies in a registry item never names react or react-dom. They are peers of
the consuming app, and a registry item that installs its own copy reproduces the "invalid hook
call" failure one layer up — see
js-library-packaging. check_package_surface enforces
this over a registry file.
Provenance. The registry-publishing shape and the deprecated-type list come from an
external plugin reference's component-CLI skill, read at a pinned revision. The source is
deliberately not named here, per
source-confidentiality: a shipped artifact does
not carry derivation attribution to a named external project. The identifier and revision
stay with the maintainer-side record.
Procedure: render a shadcn/ui component for the design brief
Step 0: Inspect
- Read
state.ui_audit.shadcn_inventory.version and confirm it matches
the version in ## Compatibility within the same major. If audit
flagged a mismatch, the user already chose to proceed — note that
in state.changes.
- Read
state.ui_audit.design_tokens — every color, spacing, and radius
in the rendered output must reference a token from this map.
- Read
state.ui_design:
components → the primitive list to compose.
microcopy → button labels, empty-state text, validation messages.
Lock — render verbatim.
states → empty / loading / error / success / disabled coverage.
a11y → ARIA labels, keyboard nav, focus order.
Step 1: Compose primitives
- Import primitives from the project's
components/ui/ path
(@/components/ui/button, …) — never from shadcn or radix-ui.
- Compose Radix-style:
<Dialog> → <DialogTrigger asChild> →
<DialogContent> → <DialogHeader> → <DialogTitle>. Never wrap
DialogTrigger around a pre-styled <button>; pass asChild.
- Use the variant API of
Button (variant="default" | "destructive" | "outline" | "secondary" | "ghost" | "link"); do not override with
raw Tailwind for the variant set.
- Forms:
useForm (react-hook-form) + zodResolver(schema) →
<Form> → <FormField> → <FormItem> → <FormLabel> →
<FormControl> → <FormMessage>. Validation messages come from
the zod schema, mirrored to the design-brief microcopy.
Step 2: Apply tokens, dark mode, a11y
- Colors via semantic classes:
bg-background, text-foreground,
bg-primary text-primary-foreground, text-muted-foreground. No
bg-white / text-black / hardcoded #fff.
- Spacing / radius from theme tokens (
rounded-lg mapped to --radius
— in the @theme block on tailwind-v4, in tailwind.config.{js,ts} on
tailwind-v3). Polish refactors hardcoded values when a token exists.
- Dark mode: never branch on a
dark prop; rely on the .dark class
on the root and semantic tokens.
- Every interactive primitive: keyboard trigger present (Enter/Space
on buttons, Esc on dialogs — free from the primitive vendor), focus ring,
aria-label from state.ui_design.a11y when icon-only.
Step 3: State coverage
- Empty: render the design-brief empty-state copy in a
Card or
inline placeholder; never null.
- Loading:
Skeleton rows for tables; Button disabled +
Loader2 icon for submit-in-flight.
- Error:
Alert variant="destructive" with the design-brief message;
FormMessage for field-level errors.
- Success:
toast.success(...) from sonner with the design-brief
confirmation copy.
- Disabled:
disabled prop on the trigger plus the design-brief
reason as aria-describedby text.
Step 4: Validate
- No raw
<input> / <button> / <select> outside the primitive set.
- No hardcoded colors / spacing — every value is a token.
- Microcopy matches
state.ui_design.microcopy byte-for-byte.
- Dark mode: toggle
.dark on <html>, render the component, every
surface still legible (no text-white on bg-white).
- Keyboard: Tab through every focusable element; focus ring visible.
Output format
- React component file(s) under the project's
components/ (or app/)
tree, importing primitives from @/components/ui/*.
- Per file, one entry recorded in
state.changes with kind="ui",
stack="react-shadcn", and the design-brief summary.
Review pass — a11y findings + preview envelope
When this skill is dispatched by directives/ui/review.ts (test slot)
or directives/ui/polish.ts (verify slot) — i.e. a review/polish run,
not the initial apply — it also emits:
state.ui_review.a11y — {violations: [{rule, selector, severity}, ...], severity_floor?, accepted_violations?}. Run an a11y tool against the
rendered output (e.g. axe-core via Playwright, @axe-core/react,
jest-axe) and translate hits into this shape. Use the same
(rule, selector) shape as state.ui_audit.a11y_baseline so the
engine's de-dup matches pre-existing entries on replay. Omit the
envelope on apply passes; the engine's _apply_a11y_gate only fires
when a baseline is present.
state.ui_review.preview — {render_ok: bool, screenshot_path?, dom_dump_path?, error?, skipped?, skip_reason?}. Render evidence is
required, not optional on a review/polish pass: you MUST drive the
headless browser (Playwright + axe-core) against the rendered output and
write render_ok. Omitting it now triggers the preview_render_required
halt — a render-capable stack can no longer claim success without
rendering. render_ok: false with error populated triggers the
preview_render_failed halt; render_ok: true with screenshot_path
threads the screenshot into the delivery report's artifacts list. The
only no-render path is an explicit, reasoned skip: set skipped: true
plus a skip_reason (e.g. no Playwright runner in this env). Browser
tooling (Playwright/Cypress/…) is a consumer-project dependency — this
package does not ship one.
Polish dispatch: when the dispatcher skips review because a previous
review pass already returned SUCCESS, this skill MUST itself
synthesise the updated state.ui_review.findings (including any
remaining a11y_violation entries) so the engine's gate sees the
current state on the next polish round.
Taste Dials
When DESIGN.md declares ## Taste Dials, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.
Component workshop (Storybook) — pointer
The workshop discipline (one concept per story, @summary on every export, stories run as
tests, the !manifest tag, the opt-in MCP channel) lives in
storybook-workshop. It is stack-agnostic and was lifted
out of here rather than duplicated beside it.
What stays React-specific and therefore stays here: the state-coverage matrix in Step 3 that
the story set is derived from, the token discipline of Step 2 that stories render under, and
the (rule, selector, severity) a11y shape in § Review pass that the workshop's validate
step writes into.
Security constraints
scripts/shadcn_add.ts is the only shipped script, and it is the single
subprocess-plus-network surface in this suite — treat it accordingly.
- What it may touch — the project rooted at
--project-root (default
cwd): it reads components.json there and lets the upstream CLI write
the generated primitives under that root. Nothing outside it.
- What it must never do — run without the exact
npx command and the
component list having been shown to the user first, per the gate above.
Never pass --overwrite unprompted; never treat a non-zero exit as
success; never work around a missing npx silently
(missing-tool-handling).
- Default invocation — mutating. A bare
shadcn_add <component>
spawns npx shadcn@latest add <component> and writes into the project.
--dry-run is the read-only path and prints the command it would run;
--list is read-only too. Use --dry-run for the proposal step.
- Outbound — yes, and this is the point of the gate:
npx resolves
shadcn@latest from the public npm registry on every live run, so both
the code fetched and the components written are chosen upstream, at run
time, not pinned here. That is the egress leg of the lethal trifecta
(lethal-trifecta-guard); the
human confirmation is what keeps it off an autonomous path.
Do NOT
- Do NOT install
shadcn-ui from npm — primitives are scaffolded.
- Do NOT hardcode colors / spacing / radii — use the token map.
- Do NOT branch on a
dark prop — use semantic tokens + the .dark class.
- Do NOT rewrite microcopy — it is locked by
state.ui_design.
- Do NOT skip
asChild on DialogTrigger / SheetTrigger / similar
Radix wrappers — it breaks the accessibility contract.
- Do NOT introduce a non-shadcn UI library (MUI, Chakra) into the same
surface — pick one system per surface.
Auto-trigger keywords
- shadcn / shadcn ui / shadcn/ui
- React component (when the project uses shadcn)
- Radix primitive
- Tailwind dark mode
- React Hook Form + zod
1---2name: react-shadcn-ui3description: Building React UI on shadcn/ui primitives + Tailwind — the apply/review/polish skill dispatched by `directives/ui/*` for the `react-shadcn` stack; idiom from tailwind-engineer.4---56# react-shadcn-ui78> **Grounded stack guidance:** pull idiomatic Do/Don't + docs URLs via9> `./scripts-run <skills-root>/corpus-grounding/scripts/ground search10> --manifest <skills-root>/design-intelligence/data/manifest.json11> --stack shadcn "<topic>"` (also `--stack react`, `--stack nextjs`). See12> [`design-intelligence`](../design-intelligence/SKILL.md).1314## Component installer — `scripts/shadcn_add.ts` (gated, assisted)1516Bundled installer (Apache-2.0-derived, see header + `design-intelligence/ATTRIBUTION.md`)17wraps `npx shadcn@latest add <components>` — **the only subprocess+network18surface in the adopted suite**. Per `runtime-safety` + the `execution`19block above:20211. **Propose, never silent-run** — always show the exact `npx` command +22 component list first (use `--dry-run`); the user confirms before any23 live run.242. **Missing tool** → per `missing-tool-handling`: if `npx`/Node is25 absent, STOP and ask (install vs. manual component copy) — never26 silently work around.273. **Verify after run** — confirm the component landed28 (`components/ui/<name>.tsx` exists, `components.json` unchanged or29 sanely updated) before reporting success.3031## Compatibility3233- **Tested against:** `shadcn@4`, Tailwind CSS `4.x`, React `19`, primitives from34 `@base-ui/react@1`, `components.json` style `base-nova` — every major read out35 of `tests/fixtures/stack/shadcn-current/package.json`, the verbatim output of36 `npx shadcn@latest init -d --template vite` on 2026-08-24. No version on this37 line may be stated from prose; move it only by re-running that scaffold and38 re-committing the fixture.39- Tailwind `3.x` and Radix primitives stay supported through the **v3 branch**40 under § Gotcha — the CLI moved on, existing projects did not.41- The audit step (`directives/ui/audit.ts`) reads the line above and42 compares it with `state.ui_audit.shadcn_inventory.version`; a major43 mismatch triggers a soft halt before this skill runs.4445## When to use4647Use when `state.stack.frontend == "react-shadcn"` and `directives/ui/apply.ts`,48`review.ts`, or `polish.ts` dispatches to this skill, or when a React project49clearly uses shadcn/ui (presence of `components.json`, a `@base-ui/react` or50`@radix-ui/*` dependency, a `components/ui/` folder of generated primitives).5152Do NOT use when:53- Project is Blade + Livewire + Flux (use `flux` / `livewire` / `blade-ui`).54- Project is Vue (use the Vue stack skills).55- Plain React without shadcn/ui — fall back to manual composition; this skill56 assumes the primitive set exists.5758## Gotcha5960- shadcn/ui is **not** an npm package. Primitives are copied into61 `components/ui/` and edited in-place. Do not `npm install shadcn-ui`.62 Run `npx shadcn@latest add <primitive>` to scaffold; then edit.63- Major-version drift between this skill's `## Compatibility` line and64 the project's installed primitives is a real risk. The audit step65 writes `state.ui_audit.shadcn_inventory` with the detected version —66 when it diverges by a major, audit emits a soft halt before this67 skill runs.68- shadcn/ui composes a primitive vendor — `@base-ui/react` in a CLI-4 scaffold,69 `@radix-ui/*` in older projects. Accessibility is built in either way, but70 only when the wrapper components are used correctly (`asChild`,71 `<DialogTrigger>` instead of a bare `<button>`).72- **Tailwind tokens: branch on the `css` axis, never guess.** `detect_stack()`73 emits `axes.css` as `tailwind-v4` or `tailwind-v3`74 (`work_engine/stack/detect.ts:521-524`); `existing-ui-audit` reads tokens on75 the same key.76 - `tailwind-v4` — **no config file exists.** Tokens live in the `@theme` block77 of the entry CSS named by `components.json` → `tailwind.css`, and that file's78 `"tailwind": {"config": ""}` — an empty `config` string — IS the v4 marker.79 Never write `tailwind.config.{js,ts}` in a v4 project.80 - `tailwind-v3` — tokens come from `tailwind.config.{js,ts}`81 (`theme.extend.colors`).82 - Either branch — CSS custom properties on `:root` and `.dark`83 (`--background`, `--foreground`, `--primary`, `--ring`, …). Audit writes them84 into `state.ui_audit.design_tokens`. Use those tokens; never hardcode values.85- Dark mode is class-based (`<html class="dark">`). Every color must come86 from `bg-background`, `text-foreground`, etc. — never raw `bg-white`.87- Every interactive primitive must declare a focus-visible state via88 `focus-visible:ring-2 focus-visible:ring-ring`; that comes for free with89 the generated primitives but is easy to remove during a refactor.90- **Anti-AI-slop: shadcn-default look.** The out-of-the-box shadcn91 theme + `Inter`-as-system-fallback + neutral grays reads as92 template across projects (catalog T7/T8 + C5). Unless93 `state.ui_audit.design_tokens` pins the neutral palette as the94 project's identity, the polish step should match typography and color95 tokens to the design brief's `aesthetic:` line (from `fe-design`96 aesthetic-direction). Theme/font drift within a single audited project97 breaks consistency — variation lives between projects, not between98 components in the same surface.99- **Anti-AI-slop catalog + linter.** Pull100 [`docs/guidelines/design-antipatterns.md`](../../../docs/guidelines/design-antipatterns.md)101 before the polish step (Visual V1–V8, Layout L1–L10 are the React-component102 slop tells); the objective quality floors (WCAG contrast, focus-visible,103 reduced-motion) are validated via `accessibility-auditor`'s checklist —104 cite its verdict rather than eyeballing.105106## Covered primitives107108This skill is validated against the following shadcn primitives at the109declared version:110111- **Form / inputs:** `Button`, `Input`, `Textarea`, `Checkbox`,112 `RadioGroup`, `Select`, `Switch`, `Label`, `Form` (react-hook-form113 wrapper + `zodResolver`).114- **Overlay:** `Dialog`, `Sheet`, `Popover`, `Tooltip`, `DropdownMenu`,115 `AlertDialog`.116- **Layout:** `Card`, `Separator`, `Tabs`, `Accordion`, `ScrollArea`.117- **Data display:** `Table` (with `@tanstack/react-table`), `Badge`,118 `Avatar`, `Skeleton`, `Progress`.119- **Feedback:** `Toast` (sonner), `Alert`.120121## Not covered — fall back to manual composition122123- Marketing-only components (Hero, Pricing, Features) — outside shadcn/ui.124- `Calendar` / `DatePicker` — composition skill required, not generated.125- `Combobox` — built from `Command` + `Popover`; case-by-case.126- Streaming / partial-prerender boundaries — use the project's framework127 patterns (Next.js / Remix), not shadcn/ui.128129## Registry & MCP awareness (opt-in)130131The default path is the bundled `scripts/shadcn_add.ts` CLI wrapper + reading132`components.json` — it works on most shadcn projects and stays the default.133The modern registry model is an **opt-in enhancement**; do not add round-trips134to every component op. Full JSON-schema + namespace detail is lazy-loaded from135[`references/registry.md`](references/registry.md) — read it only on this path,136not on the vanilla `add`.137138**`shadcn info --json` handshake** — run it as the grounding step **when** the139project declares custom/namespaced `registries` in `components.json`, OR when140theme-alignment is in scope. It returns framework, aliases, installed141components, icon lib, and base settings. Do NOT make it a forced first action142on every `add` (over-gating; low ROI on vanilla projects).143144- **Precedence vs our own audit:** prefer a live `shadcn info --json` when145 available; fall back to `state.ui_audit.shadcn_inventory` (from146 `existing-ui-audit`) when the CLI/MCP is not reachable. They answer the same147 question (project context) — the live read wins.148149**Namespaced installs** — `@ns/item` resolves via the `registries` map to a150`registry-item.json` URL (see the reference). Run `view @ns/item` to inspect151the JSON before `add`. Honour `registryDependencies` (install the graph,152including version-pinned GitHub refs like `acme/ui/button#v1.2.0`); keep153propose-never-silent-run + `--dry-run`.154155**Token-aware scaffolding** — when a `registry-item.json` carries `cssVars`156(OKLCH, light/dark/theme), align additions to the project's existing tokens157(from `info --json` / `components.json` / `state.ui_audit.design_tokens`) —158**never inject the default shadcn neutral theme** (it is a flagged anti-slop159tell: default theme + Inter fallback + neutral grays).160161**MCP path (opt-in)** — the shadcn MCP server exposes browse / search-across-162registries / install-with-natural-language over MCP; configure per the163[`mcp`](../mcp/SKILL.md) skill. It is an alternative to the CLI, never a hard164dependency. Decision note: **CLI path = default + universal; MCP path = opt-in165when the user has it configured; registry-JSON literacy underpins both.**166167## Publish a registry — the library as a source others install from168169The sections above **consume** a registry. This one **publishes** one: a component library in170this repository can expose its own components the same way, so a consumer installs them with171the tool they already use instead of copying files.1721731. **Author `registry.json`** at the library root — the index — and one174 `registry-item.json` per exposed component, each naming its files, its175 `registryDependencies`, and its `cssVars` when it carries token requirements.1762. **Build** to `public/r` with the registry build command. **The installer gate applies177 here exactly as it does to `add`**: propose it, run `--dry-run` first, and let the user178 confirm. A build writes files, so nothing about it being "our own" registry lifts the179 gate.1803. **The consumer adds a `registries` map entry** pointing at the published index; from then181 on `@ns/item` resolves through it.182183**Two registry item types are FORBIDDEN**, deprecated in v4: `registry:build` and184`registry:mcp`. Use **`registry:base`** and **`registry:font`** instead. Writing either185deprecated type produces an item the current CLI does not understand, and the failure surfaces186at the consumer rather than at authoring time.187188**`dependencies` in a registry item never names `react` or `react-dom`.** They are peers of189the consuming app, and a registry item that installs its own copy reproduces the "invalid hook190call" failure one layer up — see191[`js-library-packaging`](../js-library-packaging/SKILL.md). `check_package_surface` enforces192this over a registry file.193194> **Provenance.** The registry-publishing shape and the deprecated-type list come from an195> external plugin reference's component-CLI skill, read at a pinned revision. The source is196> deliberately not named here, per197> [`source-confidentiality`](../../rules/source-confidentiality.md): a shipped artifact does198> not carry derivation attribution to a named external project. The identifier and revision199> stay with the maintainer-side record.200201## Procedure: render a shadcn/ui component for the design brief202203### Step 0: Inspect2042051. Read `state.ui_audit.shadcn_inventory.version` and confirm it matches206 the version in `## Compatibility` within the same major. If audit207 flagged a mismatch, the user already chose to proceed — note that208 in `state.changes`.2092. Read `state.ui_audit.design_tokens` — every color, spacing, and radius210 in the rendered output must reference a token from this map.2113. Read `state.ui_design`:212 - `components` → the primitive list to compose.213 - `microcopy` → button labels, empty-state text, validation messages.214 **Lock — render verbatim.**215 - `states` → empty / loading / error / success / disabled coverage.216 - `a11y` → ARIA labels, keyboard nav, focus order.217218### Step 1: Compose primitives2192201. Import primitives from the project's `components/ui/` path221 (`@/components/ui/button`, …) — never from `shadcn` or `radix-ui`.2222. Compose Radix-style: `<Dialog>` → `<DialogTrigger asChild>` →223 `<DialogContent>` → `<DialogHeader>` → `<DialogTitle>`. Never wrap224 `DialogTrigger` around a pre-styled `<button>`; pass `asChild`.2253. Use the variant API of `Button` (`variant="default" | "destructive" |226 "outline" | "secondary" | "ghost" | "link"`); do not override with227 raw Tailwind for the variant set.2284. Forms: `useForm` (react-hook-form) + `zodResolver(schema)` →229 `<Form>` → `<FormField>` → `<FormItem>` → `<FormLabel>` →230 `<FormControl>` → `<FormMessage>`. Validation messages come from231 the zod schema, mirrored to the design-brief microcopy.232233### Step 2: Apply tokens, dark mode, a11y2342351. Colors via semantic classes: `bg-background`, `text-foreground`,236 `bg-primary text-primary-foreground`, `text-muted-foreground`. No237 `bg-white` / `text-black` / hardcoded `#fff`.2382. Spacing / radius from theme tokens (`rounded-lg` mapped to `--radius`239 — in the `@theme` block on `tailwind-v4`, in `tailwind.config.{js,ts}` on240 `tailwind-v3`). Polish refactors hardcoded values when a token exists.2413. Dark mode: never branch on a `dark` prop; rely on the `.dark` class242 on the root and semantic tokens.2434. Every interactive primitive: keyboard trigger present (Enter/Space244 on buttons, Esc on dialogs — free from the primitive vendor), focus ring,245 `aria-label` from `state.ui_design.a11y` when icon-only.246247### Step 3: State coverage2482491. Empty: render the design-brief empty-state copy in a `Card` or250 inline placeholder; never `null`.2512. Loading: `Skeleton` rows for tables; `Button` `disabled` +252 `Loader2` icon for submit-in-flight.2533. Error: `Alert variant="destructive"` with the design-brief message;254 `FormMessage` for field-level errors.2554. Success: `toast.success(...)` from `sonner` with the design-brief256 confirmation copy.2575. Disabled: `disabled` prop on the trigger plus the design-brief258 reason as `aria-describedby` text.259260### Step 4: Validate2612621. No raw `<input>` / `<button>` / `<select>` outside the primitive set.2632. No hardcoded colors / spacing — every value is a token.2643. Microcopy matches `state.ui_design.microcopy` byte-for-byte.2654. Dark mode: toggle `.dark` on `<html>`, render the component, every266 surface still legible (no `text-white on bg-white`).2675. Keyboard: Tab through every focusable element; focus ring visible.268269## Output format2702711. React component file(s) under the project's `components/` (or `app/`)272 tree, importing primitives from `@/components/ui/*`.2732. Per file, one entry recorded in `state.changes` with `kind="ui"`,274 `stack="react-shadcn"`, and the design-brief summary.275276### Review pass — a11y findings + preview envelope277278When this skill is dispatched by `directives/ui/review.ts` (test slot)279or `directives/ui/polish.ts` (verify slot) — i.e. a review/polish run,280not the initial apply — it also emits:281282- `state.ui_review.a11y` — `{violations: [{rule, selector, severity}, ...],283 severity_floor?, accepted_violations?}`. Run an a11y tool against the284 rendered output (e.g. `axe-core` via Playwright, `@axe-core/react`,285 `jest-axe`) and translate hits into this shape. Use the same286 `(rule, selector)` shape as `state.ui_audit.a11y_baseline` so the287 engine's de-dup matches pre-existing entries on replay. Omit the288 envelope on apply passes; the engine's `_apply_a11y_gate` only fires289 when a baseline is present.290- `state.ui_review.preview` — `{render_ok: bool, screenshot_path?,291 dom_dump_path?, error?, skipped?, skip_reason?}`. **Render evidence is292 required, not optional** on a review/polish pass: you MUST drive the293 headless browser (Playwright + axe-core) against the rendered output and294 write `render_ok`. Omitting it now triggers the `preview_render_required`295 halt — a render-capable stack can no longer claim success without296 rendering. `render_ok: false` with `error` populated triggers the297 `preview_render_failed` halt; `render_ok: true` with `screenshot_path`298 threads the screenshot into the delivery report's `artifacts` list. The299 only no-render path is an **explicit, reasoned skip**: set `skipped: true`300 plus a `skip_reason` (e.g. no Playwright runner in this env). Browser301 tooling (Playwright/Cypress/…) is a consumer-project dependency — this302 package does not ship one.303304Polish dispatch: when the dispatcher skips `review` because a previous305review pass already returned `SUCCESS`, this skill MUST itself306synthesise the updated `state.ui_review.findings` (including any307remaining `a11y_violation` entries) so the engine's gate sees the308current state on the next polish round.309310## Taste Dials311312When `DESIGN.md` declares `## Taste Dials`, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.313314## Component workshop (Storybook) — pointer315316The workshop discipline (one concept per story, `@summary` on every export, stories run as317tests, the `!manifest` tag, the opt-in MCP channel) lives in318[`storybook-workshop`](../storybook-workshop/SKILL.md). It is stack-agnostic and was lifted319out of here rather than duplicated beside it.320321What stays React-specific and therefore stays here: the state-coverage matrix in Step 3 that322the story set is derived from, the token discipline of Step 2 that stories render under, and323the `(rule, selector, severity)` a11y shape in § Review pass that the workshop's validate324step writes into.325326## Security constraints327328`scripts/shadcn_add.ts` is the only shipped script, and it is the single329subprocess-plus-network surface in this suite — treat it accordingly.330331- **What it may touch** — the project rooted at `--project-root` (default332 `cwd`): it reads `components.json` there and lets the upstream CLI write333 the generated primitives under that root. Nothing outside it.334- **What it must never do** — run without the exact `npx` command and the335 component list having been shown to the user first, per the gate above.336 Never pass `--overwrite` unprompted; never treat a non-zero exit as337 success; never work around a missing `npx` silently338 ([`missing-tool-handling`](../../rules/missing-tool-handling.md)).339- **Default invocation** — mutating. A bare `shadcn_add <component>`340 spawns `npx shadcn@latest add <component>` and writes into the project.341 `--dry-run` is the read-only path and prints the command it would run;342 `--list` is read-only too. Use `--dry-run` for the proposal step.343- **Outbound** — yes, and this is the point of the gate: `npx` resolves344 `shadcn@latest` from the public npm registry on every live run, so both345 the code fetched and the components written are chosen upstream, at run346 time, not pinned here. That is the egress leg of the lethal trifecta347 ([`lethal-trifecta-guard`](../../rules/lethal-trifecta-guard.md)); the348 human confirmation is what keeps it off an autonomous path.349350## Do NOT351352- Do NOT install `shadcn-ui` from npm — primitives are scaffolded.353- Do NOT hardcode colors / spacing / radii — use the token map.354- Do NOT branch on a `dark` prop — use semantic tokens + the `.dark` class.355- Do NOT rewrite microcopy — it is locked by `state.ui_design`.356- Do NOT skip `asChild` on `DialogTrigger` / `SheetTrigger` / similar357 Radix wrappers — it breaks the accessibility contract.358- Do NOT introduce a non-shadcn UI library (MUI, Chakra) into the same359 surface — pick one system per surface.360361## Auto-trigger keywords362363- shadcn / shadcn ui / shadcn/ui364- React component (when the project uses shadcn)365- Radix primitive366- Tailwind dark mode367- React Hook Form + zod