Recomponentize ui
Refactor a project's UI into atomic components (atoms/molecules/organisms/templates), sourcing primitives from a design-system registry when one is configured (default @aihero) or stock shadcn otherwise, and codemod off-token styling. Use when asked to recomponentize, refactor UI, adopt a design system, or clean up component structure.From its SKILL.md
npx -y skills add ai-hero/hero-skills --skill recomponentize-uiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
17.2 KB, as published. Nobody here has run it
Recomponentize UI — Atomic Components, Design-System Sourced
Refactor an app's UI into a proper atomic component hierarchy, and stop hand-rolling primitives that already exist upstream.
Two things always happen: components get recomponentized into atomic layers, and off-token styling gets codemodded. A third happens when a registry is available: local primitives get replaced by registry components.
Pipeline DAG
preflight → enforce → inventory → map → install → recomponentize → codemod → verify
Print the DAG line at the start of each step:
[N/8] (✓) preflight → (▶) enforce → ( ) inventory → ( ) map → ( ) install → ( ) recomponentize → ( ) codemod → ( ) verify
Now running: enforce
Enforcement is installed early, before any code is rewritten, so the path-scoped rule guides the migration itself rather than only future work.
When no registry is configured, map and install still run — against stock
shadcn or the project's existing UI library instead of a private registry.
Arguments
$ARGUMENTS:- (none) — full pass using the component source resolved in Step 0
--audit-only— runpreflight,inventory,map; report the plan, change nothing. Skipsenforcetoo: installing the rule and hook writes files, which--audit-onlypromises not to do.REGISTRY_NAMESPACE— override the registry (e.g.@acme)
Step 0: Resolve the component source
Producer repos must opt out — check this first
If HERO.md says role: producer (or enabled: false) under ## Design System, stop immediately. Report that this repo publishes the design system
and exit without changing anything. A registry repo's pipeline runs mockup →
design system; running this skill there would invert it and try to consume its
own output.
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
# Reads a whole SECTION, not a scalar field — hero_field cannot express this.
sed -n '/## Design System/,/^## /p' "$ROOT/HERO.md" 2>/dev/null `# hero-lint: allow-inline` || echo "NO_DESIGN_SYSTEM_CONFIG"
A repo is a producer if it builds a registry.json, serves /r/*, or its
components.json aliases ui to an internal atomic directory rather than
@/components/ui. If those signals are present but HERO.md says consumer,
trust the signals, stop, and tell the user HERO.md looks wrong.
Pick the component source
Resolve in this order and state which one you picked before proceeding:
| Condition | Source |
|---|---|
HERO.md ## Design System present with namespace | That registry |
| No config, but the user wants one | Offer @aihero / https://design.aihero.studio — needs a token (Step 1) |
No registry, components.json exists | Stock shadcn — npx shadcn@latest add ITEM |
No registry, another UI lib in package.json (MUI, Chakra, Mantine, Ant) | That library's primitives; do not migrate libraries uninvited |
| Nothing — plain HTML/CSS | Recomponentize and codemod only; ask before introducing any dependency |
Expected HERO.md keys: namespace, registry-url, token-env-var, docs,
atomic-layers. AI Hero defaults:
- namespace:
@aihero - registry-url:
https://design.aihero.studio/r/{name}.json - token-env-var:
REGISTRY_TOKEN - docs:
https://design.aihero.studio
If the registry publishes a consumer handbook, read it first and let it win
over this skill. For @aihero that is handbook/consuming-the-registry.md in
the ai-hero/design-system repo — it ships a canonical consumer AGENTS.md stanza
and consumer SKILL.md. Install those rather than re-deriving them.
Never introduce a UI library into a project that has none without asking. The atomic refactor is valuable on its own and carries no new dependency.
Step 1: Preflight (preflight)
Skip to Step 2 when the source is "recomponentize only" — the enforcement layer still applies. Otherwise, never run
npx shadcn init on an existing project — it does not add the registries block
and may pick a conflicting style.
| Check | Requirement |
|---|---|
components.json | Has a registries block for the namespace; "ui": "@/components/ui" |
| Token expansion | Header is Bearer ${REGISTRY_TOKEN} — the plain form ONLY |
.env | Holds the token; .gitignore covers .env BEFORE the token is written |
src/lib/utils.ts | Exports cn (clsx + tailwind-merge) |
tsconfig.json | baseUrl: "." and paths: { "@/*": ["./src/*"] } |
| CSS entry | Imports tailwind (v4); named in components.json → tailwind.css |
| Runtime deps | react@^19, react-dom@^19, tailwindcss@^4, clsx@^2, tailwind-merge@^3, shadcn@^4.13.0 |
Trap — never write ${VAR:-default}. The CLI's expansion regex is
/\$\{(\w+)\}/g, so the default form ships as a literal header string and
surfaces as a confusing 401 instead of a missing-variable error.
Trap — do not copy the design system's own components.json. A registry
repo's config points at localhost and aliases ui to its internal atomic dir.
Those are wrong in a consumer.
If the project has a committed .env, rename it to .env.example, strip the
value, and gitignore the real one. Never commit a token.
Trap — .env must sit next to components.json, not in the cwd. Verified
empirically against shadcn 4.13:
.env location | cwd | Result |
|---|---|---|
| repo root | repo root, -c ui | fails |
| repo root | ui/ | fails |
ui/ | repo root, -c ui | works |
ui/ | ui/ | works |
none, REGISTRY_TOKEN exported | anywhere | works |
So in a monorepo whose UI lives in ui/, the token goes in ui/.env even when
the repo's house convention keeps every other secret in a root .env. A root
.env produces Set the required environment variables to your .env or .env.local file — which reads like a missing variable rather than a
wrong-directory problem, so it is easy to misdiagnose. Exporting the variable in
the shell or a task-runner recipe also works and beats duplicating the secret.
Confirm the wiring before going further — install the theme item first so tokens land before any component references them:
npx shadcn@latest add NAMESPACE/theme
grep -- "--primary" src/styles.css
Step 2: Install enforcement (enforce)
A skill only fires when the model chooses it, and model-discretion triggering is least reliable for exactly this kind of well-trained task. These layers do not depend on that choice — install both:
"$PLUGIN_ROOT/scripts/install-design-system.sh" "$ROOT"
It writes, without overwriting customized files (exit 2 on drift, same contract
as install-auto-approve.sh):
.claude/rules/design-system.md— path-scoped to**/*.{tsx,jsx,css}, so the constraints load whenever Claude reads a UI file rather than when a description happens to match..claude/hooks/check-design-tokens.sh+PostToolUsewiring — flags raw hex, palette classes, and component-root margins on write.
Then add the registry's AGENTS.md stanza (Part 8 of the @aihero handbook) and
the consumer SKILL.md (Part 9). If the registry ships them, paste verbatim.
Optionally port the registry's lint config (for @aihero,
eslint.taste.config.mjs — Tailwind correctness, token discipline, a11y floor).
Its atomic-boundaries block does apply once Step 6's layers exist; add ui
and blocks as the lowest elements in the layer matrix.
Step 3: Inventory the current UI (inventory)
Find every hand-rolled element, every oversized component, and every off-token style. Use an Explore subagent for a large codebase.
# Hand-rolled primitives that likely exist upstream
grep -rnE '<(button|input|select|textarea|table|dialog|nav|header|footer)\b' --include="*.tsx" --include="*.jsx" src/
# Recomponentization candidates — size outliers
find src -name "*.tsx" -exec wc -l {} + | sort -rn | head -20
# Off-token styling — the codemod targets from Step 7
grep -rnE 'className="[^"]*\b(bg|text|border|ring)-(slate|gray|zinc|neutral|stone|red|blue|green|amber)-[0-9]' --include="*.tsx" src/
grep -rnE '#[0-9a-fA-F]{3,8}\b|oklch\(' --include="*.tsx" --include="*.ts" src/
grep -rnE 'className="[^"]*\bshadow-|z-\[|rounded-\[|text-\[[0-9]' --include="*.tsx" src/
grep -rnE 'className="[^"]*\bdark:(bg|text|border)-' --include="*.tsx" src/
Record for each finding: file, line, what it is, and the UI concept it expresses ("a primary action", "a labelled form field with error text"). The concept — not the markup — is what you search for in Step 4.
Step 4: Map local → upstream (map)
For every concept in the inventory, find its upstream equivalent. Use both paths — they surface different things:
Search the catalog (finds by category keyword):
npx shadcn@latest search NAMESPACE -q "form"
npx shadcn@latest view NAMESPACE/field # inspect the API before committing
There is no --registry flag; registries come only from components.json. For
@aihero the full catalog is GET /r/registry.json — there is no
/r/index.json.
Browse the docs/gallery site (finds by appearance): open it with the browser
tools and look at the rendered components. Search matches keywords; the gallery
matches look. A local "stat card" may be a tile or a kpi-strip — only the
gallery makes that obvious. For stock shadcn, use ui.shadcn.com/docs/components.
Optionally wire the registry's MCP server for in-editor search:
npx shadcn@latest mcp init --client claude
It reads components.json for registries and auth headers — no separate MCP
auth. If /mcp reports no tools, run npx clear-npx-cache.
Produce a mapping table and show it to the user before installing:
LOCAL → UPSTREAM ITEM CONFIDENCE NOTE
src/components/Btn.tsx → @aihero/button high variant=default|destructive
src/components/StatCard.tsx → @aihero/tile medium confirm against gallery
src/components/Wizard.tsx → (none) — keep; recomponentize only
Items with no upstream equivalent stay local — they still get recomponentized (Step 6) and codemodded (Step 7). Do not force a bad match.
Step 5: Install and rewrite call sites (install)
npx shadcn@latest add NAMESPACE/button NAMESPACE/card
npx shadcn@latest add NAMESPACE/button --dry-run --diff # review a re-add
registryDependencies resolve transitively — asking for field also brings
label and separator. add defaults to --overwrite false.
Installed files are vendored, not authored. This is the rule that matters
most: rewrite call sites, never the vendored files. A local edit to
components/ui/* is silently reverted by the next shadcn add --overwrite. To
change a vendored component: compose around it, pass className (every item
merges via cn()), or change it upstream and re-add.
Delete the local component only after its last call site is migrated and typecheck passes.
Step 6: Recomponentize (recomponentize)
This step always runs — it is the point of the skill. It runs for components with no upstream match, for projects with no registry at all, and for the app code that composes vendored primitives. Swapping in new components without recomponentizing leaves the same monolith wearing new classes.
Target layout — vendored code stays flat; the app's own components go atomic:
src/components/
├── ui/ # vendored primitives (registry or shadcn) — never edit
├── blocks/ # vendored sections — never edit
├── atoms/ # app-specific primitives upstream lacks (should be rare)
├── molecules/ # compositions of ui/ + atoms/
├── organisms/ # domain-aware sections; may import domain types
└── templates/ # layout shells with slot props — structure only, zero copy
Layer rules, enforced in review:
- atoms / molecules are stateless and generic: no fetching, no auth, no domain
types. Props in, UI out. Every one accepts and merges
className. - organisms may import domain types and compose molecules — still no data fetching; data arrives via props.
- templates define where things go via slot props (
header,sidebar,children); they never hardcode copy or fetch data. - Imports flow downward only: templates → organisms → molecules → atoms →
(
ui/,blocks/).ui/andblocks/are the floor — any layer may import them. An atom importing a molecule is a defect; restructure instead of suppressing it. - Same-layer imports only for family relationships. Test: can you describe
the importer without naming a different concept? "A row of buttons" is still
buttons → atom. The moment a component combines distinct siblings
(
input+button=search-bar), it belongs one layer up. - Place each component at the lowest layer that fits. Promote only when it gains domain knowledge or composition — never preemptively.
Signals a component needs recomponentizing: boolean-prop explosion, a molecule fetching data, two organisms sharing copy-pasted JSX, a component importing from a higher layer, a file well above the codebase's median length.
Migrate call sites as you move files; never leave a re-export shim behind as "temporary" — finish the move or don't start it.
Step 7: Codemod off-token styling (codemod)
| Found | Replace with |
|---|---|
Raw palette (bg-zinc-100, text-gray-500) | Semantic token (bg-muted, text-muted-foreground) |
Hex / oklch() literal in TSX | A token in the @theme layer |
dark: color override | Delete it — a dark: color means the wrong token was used |
Margin on a component root (m-*, mt-*, ms-*) | gap-* / space-* on the parent; parents own layout |
Arbitrary spacing (p-[13px], gap-[7px]) | The spacing scale; if a step is missing, change the scale |
z-[9999] | The named z-scale (z-dropdown < z-sticky < z-overlay < z-modal < z-toast) |
rounded-[10px], text-[15px], any [Npx] | The radius / type / spacing scale |
shadow-* | Remove — elevation is borders and hairlines (@aihero house rule) |
| Non-Lucide icons | Lucide, sized size-4 / size-5, aria-hidden unless it is the only label |
| Text on a colored surface | The paired foreground token (bg-primary → text-primary-foreground) |
| Removed focus outline | A visible focus-visible: ring — removing one without a replacement is a defect |
| Opacity hack for disabled | disabled: variants + aria-disabled semantics |
Also: variants via cva with typed props, never ternary/string-concat className
soup. Never hand-sort classes — prettier-plugin-tailwindcss owns the order.
The no-shadow rule and the exact token vocabulary are registry-specific. With
stock shadcn, keep its default token names (bg-background,
text-muted-foreground are shared) and do not strip shadows — that is an
@aihero house rule, not a shadcn one.
Step 8: Verify (verify)
npx tsc --noEmit
Then, using the browser tools, render the migrated screens and confirm:
- Each replaced component renders correctly in light and dark. Dark mode is
the
.darkclass — neverprefers-color-scheme. - Transitive deps arrived (installing
fieldmust also producelabel.tsxandseparator.tsx). grep -- "--primary" CSS_ENTRYfinds the theme's variables.- No visual regression on screens you did not intend to touch.
- No import-direction violations: nothing in
atoms/imports frommolecules/or above.
Run the project's own lint/typecheck/test commands from HERO.md before
reporting done.
Report
Recomponentize UI Summary
=========================
Source: NAMESPACE (REGISTRY_URL) | stock shadcn | recomponentize-only
Components: N replaced upstream, M kept local
Recomponentized: N files → atoms/molecules/organisms/templates
Codemods: N palette → token, N margins → parent gap, N arbitrary values
Enforcement: .claude/rules/design-system.md, PostToolUse hook, AGENTS.md stanza
Verified: tsc clean · light+dark checked on N screens · import direction clean
Unmapped (kept local, recomponentized only):
- PATH — reason
Next step: hero-skills:push-pr
Key Principles
- Recomponentizing is the job. Sourcing from a registry is the bonus, not the point. A pass that swaps components without restructuring has failed.
- Search before you build. Hand-rolling an existing component is the defect this skill prevents.
- Vendored, not authored. Rewrite call sites; never edit
ui/orblocks/. - The concept, not the markup. Search for what an element means.
- Gallery and search are complementary. Search matches keywords; the gallery matches appearance.
- Never force a match, never add a library uninvited. No upstream equivalent → keep it local, recomponentize it, and say so.