Help us improve
Share bugs, ideas, or general feedback.
From html-componentize
Convert static HTML+CSS into React or Vue components deterministically, with fidelity verified by render-and-pixel-diff. Use when the user wants to turn an HTML page/mockup/template into framework components, migrate legacy HTML to a component library, or "make this HTML a React/Vue component". Preserves authored CSS (no computed-style inlining), reuses existing components via an index, and proves the result renders identically.
npx claudepluginhub mzd-hseokkim/html-componentize --plugin html-componentizeHow this skill is triggered — by the user, by Claude, or both
Slash command
/html-componentize:html-componentizeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A scripted pipeline. The deterministic work (parse, classify, diff, CSS move,
examples/existing-project/package.jsonexamples/existing-project/src/Layout.tsxexamples/existing-project/src/components/Card.module.cssexamples/existing-project/src/components/Card.tsxexamples/existing-project/src/index.cssexamples/lint-cases/README.mdexamples/lint-cases/src/components/cart/StatesPreview/StatesPreview.tsxexamples/lint-cases/src/components/mypage/MyPage.tsxexamples/lint-cases/src/pages/CartPage.tsxexamples/lint-cases/src/pages/HomePage.test.tsxexamples/sample/index.htmlexamples/sample/variant.htmlreferences/interview.mdreferences/principles.mdscripts/css-to-modules.mjsscripts/detect-boundaries.mjsscripts/detect-project.mjsscripts/extract-data.mjsscripts/index-workspace.mjsscripts/lib/domtree.mjsApplies C++ Core Guidelines to write, review, or refactor C++ code. Enforces modern, safe, and idiomatic practices for C++17/20/23.
Share bugs, ideas, or general feedback.
A scripted pipeline. The deterministic work (parse, classify, diff, CSS move,
index, screenshot-diff) is done by Node scripts in scripts/; you (the model)
do judgment (confirm boundaries, name, wire JSX/SFC, debug verify failures).
Read references/principles.md first. The one rule: restructure, never
re-author. Authored CSS, source text, and DOM structure are MOVED verbatim by
scripts — never regenerated by you. computed style is used only by phase 5 as a
verification oracle, never to write code.
Scripts ship with this skill, but commands run from the USER's project cwd. Set
a variable to this skill's scripts dir and use it (prefix $SCRIPTS/) for every
node scripts/... command below:
SCRIPTS="$CLAUDE_PLUGIN_ROOT/skills/html-componentize/scripts"SCRIPTS=<repo>/skills/html-componentize/scriptsArtifacts are written to .componentize/ in the user's project (cwd). All
scripts print a compact JSON summary and write a full JSON artifact.
cd "$SCRIPTS" && npm install # cheerio, postcss, playwright, babel, etc.
npx playwright install chromium # needed only for phase 5 verification
The skill usually runs INSIDE the target project, so detect first:
node scripts/detect-project.mjs --root . --out .componentize/detected.json
This infers framework, language, styling, mode, indexRoot, componentsDir, AND
routing/layout convention (routing = react-router/next/vue-router/nuxt +
existing layout path; layoutStrategy = reuse-layout | hoist | inline) from
package.json + config files + a file scan.
Detection is a fast-path HINT from known conventions, not the source of truth
(routing.basis = "known-conventions"). When routing.layoutPath is null, the
router is unusual, or the project is non-standard, VERIFY and extend it yourself:
scan the workspace index for a kind:"layout" component, read the router config,
check the app entry — then decide. Don't treat a hardcoded miss as "no layout".
(plan-files already cross-checks the scanned index for a layout.) Then:
references/interview.md ONLY for fields not detected or
listed under ambiguities (e.g. both react+vue present, Tailwind in use,
outDir, granularity, asset strategy, viewports).
Write the merged result to .componentize/config.json.
GATE: framework + styling + mode + outDir settled. Never guess a detected-as-
ambiguous field — confirm it. Detected-with-high-confidence fields need only a
confirm, not a question.node scripts/index-workspace.mjs --root <indexRoot> --out .componentize/workspace-index.json
Builds the living index of existing components (name, props, structural signature). Skip for greenfield. GATE: index exists if mode=integrate.
node scripts/parse-source.mjs --html <file> --out .componentize/source-map.json
DOM tree + authored CSS rules + assets + flagged interactions. Also captures:
node.rawHTML for inline svg/math (icons) — verbatim outerHTML, so
glyphs aren't dropped. Render these faithfully in phase 4 (see PATTERN.md).document.fontLinks / headLinks / fontFaceCount — <head> font assets
that live outside the body and MUST be wired into the app (phase 4), else the
font falls back and renders wrong.
GATE: parse ok; note any rawMarkupNodes and fonts in the summary.node scripts/detect-boundaries.mjs --in .componentize/source-map.json \
--out .componentize/boundaries.json [--index .componentize/workspace-index.json]
# RE-CONVERTING the same page? add: --manifest .componentize/manifest.json --self <source.html>
# (excludes THIS page's own prior output from reuse-matching so it regenerates,
# instead of matching itself as "reuse")
Labels each node layout | reuse | new-component | leaf-markup, finds
repetition groups (structural-hash), matches the index for reuse.
Your job: review boundaries.json — confirm/override low-confidence labels
(confidence < 0.5), confirm component names. GATE: every node has a label you
accept; layout vs component decided.
node scripts/extract-data.mjs --map .componentize/source-map.json \
--boundaries .componentize/boundaries.json --out .componentize/data-spec.json
For each repetition group: tree-diff the instances → props + a data array with values copied VERBATIM. GATE: data values match the source character-for-character (the script copies them; do not edit them when you emit the data file).
First, plan the directory layout (don't dump files flat):
node scripts/plan-files.mjs --boundaries .componentize/boundaries.json \
--data .componentize/data-spec.json --config .componentize/config.json \
--index .componentize/workspace-index.json --manifest .componentize/manifest.json \
--sharedDir <config.sharedDir> --out .componentize/file-plan.json
file-plan.json gives each component its target folder + file paths and import
graph. Default structure is co-location (one folder per component:
Card/{Card.tsx, Card.module.css, index.ts}; the list container also gets its
*.data.ts). Honor config.structure (co-location | nested | flat).
Shared components (file-plan sharedDir + per-component shared) — page
chrome and general primitives (Icon/Button/StatePanel/Skeleton/…) are placed in
sharedDir (default <componentsDir>/common), NOT the page folder, so later
pages never couple to a component buried in the first page's folder. Honor each
component's shared flag and put its files (component + .module.css + CSS) at
its planned dir. The shared Layout (hoist) also lands in sharedDir.
Re-conversion (file-plan.reconvert + per-component writeMode) — when a
target already exists, do NOT blind-overwrite. Honor writeMode:
create — write fresh.overwrite — we generated it and it's unchanged → safe to regenerate.reconcile — we generated it but it was HAND-EDITED since. Surgically
update: regenerate only the deterministic parts (the .module.css via
css-to-modules, the data file, props/structure to match the new source) and
apply them to the existing file, PRESERVING manual edits (handlers, extra
props) that don't conflict. Show the user a diff.foreign — a file we never generated. Never overwrite. Surface it to the
user and ask (rename, pick another path, or explicit overwrite).
This is the default surgical update behavior — update existing source to
match reality, not blind regeneration.Layout (file-plan.layout) — page chrome (header/nav/footer, marked
shellRole:'chrome') does NOT get duplicated into the page:
reuse-layout: render ONLY the page content into the existing layout's outlet
(routing.outlet, e.g. <Outlet/>, <router-view/>, Next layout children).
Don't regenerate chrome; reconcile only if the layout lacks it.hoist: generate the planned shared Layout with an outlet; the page becomes
a route component holding only its content. Wire per routing.library.inline: standalone page — chrome stays in the page component.
Follow file-plan.layout.instruction.Then, for each planned component (new-component and list-container layout):
styles path:
node scripts/css-to-modules.mjs --map .componentize/source-map.json \
--classes "<comma,sep,classes,in,subtree>" --out <plan.files.styles> \
--globals <outDir>/global.css
plan.files.component using templates/<framework>/PATTERN.md
as the exact idiom: className={styles.x} / :class, props from
data-spec.json, static text inline. Emit the data file (plan.files.data)
and the index re-export. Wire imports per plan.imports.rawHTML, render it verbatim
(React dangerouslySetInnerHTML, Vue inline <svg>/v-html). Never emit an
empty element where the source had an icon.document.fontLinks/headLinks into the app
index.html <head> and import the global stylesheet (global.css, holds
@font-face) at the app entry.detected.scaffoldStyles): npm create vite/CRA
ship index.css/App.css with opinionated defaults — button{padding:.6em 1.2em},
:root{}, body{place-items:center}, #root{max-width;padding} — that OVERRIDE
the UA defaults the source relied on and silently break layout (e.g. an icon
button's padding eats its width → the inner <svg> collapses to a few px).
Remove or override these boilerplate rules so the authored CSS fully owns base
styling. Flag what you removed in the unknowns ledger.
GATE: files land at their planned paths; every rawHTML node rendered; head
font assets wired; scaffold defaults neutralized; no declaration is sourced from
computed style; CSS came only from the .module.css the script emitted.For every node labeled reuse: import the indexed component, map data-spec
fields onto its existing props. Record reuse-vs-generate in
.componentize/reuse-decision.json.
Reuse-time hoist (file-plan.hoistPlan) — when a reused component is
ownership:"page-owned" (lives in another page's folder), reusing it in-place
couples pages. Per hoistPolicy (chrome-always | on-second-use | manual),
hoist it to sharedDir as ONE op (use writeMode:reconcile — surgical + diff):
(a) move pageA/X → common/X (component and its .module.css/CSS — P6:
don't leave shared styles duplicated in two page globals),
(b) rewrite the original owner's imports + drop X from its barrel,
(c) the new consumer imports X from common,
(d) update workspace-index + manifest canonical path to common so the
3rd page reuses from there.
Partial-shared (P5): if only PART of a subtree is shared (e.g. StatePanel
shell shared, EmptyState/ErrorState copy page-specific), hoist the reused
node only and keep the page-specific wrapper local — boundaries is node-level.
Icon dedup (P4): identical rawHTML icons across pages → one common/icons
set (match by rawHTML signature); keep page-only icons local.
GATE: nothing is generated that already exists in the index; no shared component left page-owned. Re-index after generating/hoisting:
node scripts/index-workspace.mjs --root <indexRoot> --out .componentize/workspace-index.json --tag generated
Snapshot the files this run produced so a future re-conversion can tell overwrite/reconcile/foreign apart:
node scripts/manifest.mjs --plan .componentize/file-plan.json --source <source.html> \
--out .componentize/manifest.json
GATE: manifest updated after every codegen.
Catch page→page coupling (a missed hoist):
node scripts/lint-deps.mjs --root <indexRoot> --out .componentize/coupling-report.json
Exit 1 = violations (a page imports a component from another page's folder).
Surface them (also copy into reuse-decision.json → couplingViolations) and
hoist the offending component to sharedDir. GATE: no coupling violations, or
each is explicitly accepted by the user.
Spin up a dev server rendering the converted top-level component in isolation (e.g. a Vite page that imports it), then:
node scripts/verify-fidelity.mjs --original <source.html> --result <http://localhost:PORT> \
--viewports <from config> --threshold <from config> --out .componentize/verify
It waits for web fonts (document.fonts.ready) before shooting, captures the
full page (not just above-the-fold; --viewport-only to opt out), and gates
on THREE signals so subtle/localized differences can't slip through:
--threshold (default 1%), AND--blockThreshold (default 40% — a single
missing/wrong element like an icon or panel lights up its block), ANDdomMatch:false — e.g. a dropped inline
SVG makes the DOM differ).Besides verify-report.json and the per-viewport PNGs, it writes a
self-contained .componentize/verify/report.html — side-by-side original /
result / diff per viewport with global %, worst-block %, DOM match, and the
failReason. Always point the user to it.
Exit 0 = pass. On fail, read localDiff.failReason, open report.html, locate
the region, fix the cause (missed CSS rule, wrong class scope, dropped inline
SVG/icon, unwired web font, or a layout node treated as a component), and
re-run. Do not declare success until phase 5 passes.
Maintain .componentize/unknowns.md: anything flagged — interactions from
phase 1, unmappable reuse props, low-confidence boundaries you couldn't resolve,
verify regions you couldn't close. Surface it to the user. Flag, don't guess.
.componentize/verify/report.html (the
side-by-side comparison report).verify-report.json.