html-to-component
Convert HTML (a whole page, a pasted section, or a fetched URL) into Next.js + Tailwind + shadcn/ui that looks production-grade and fits the project's existing structure. It does two jobs in order: first it lifts the design's tokens into the theme (colors in oklch, fonts, radius, shadows, spacing, gradients) so everything downstream is token-based; then it turns the markup into components without creating duplicates — the headline being no duplicate Header, no second Avatar, no third Card.
The hard part isn't generating JSX from HTML — current models do that well. The hard parts are: (a) not hardcoding the raw #3B82F6/14px/box-shadow the HTML is full of, but routing them through a real theme; and (b) not re-emitting the same header/card/avatar inline on every section. So this skill is built around a theme-first, then dedup-first discipline.
This is a builder skill. It assumes the project is already scaffolded (nextjs-bootstrap) — it builds into that structure. It does not scaffold.
The two core problems this skill solves
- Raw values everywhere. HTML/CSS hands you literal hex, px, rgba shadows, inline styles, or baked-in Tailwind arbitrary values (
bg-[#3B82F6],text-[14px]). Shipped as-is, the redesign never survives and shadcn components stay unthemed. The fix: extract the recurring values as tokens and write them where a shadcn app stores its theme —globals.css(colors in oklch) andlayout.tsx(fonts) — then style components withbg-primary/rounded-lg/shadow-card, never the raw value. - Designs repeat. The same header sits on five sections, avatars and cards are everywhere. A naive markup→code pass re-emits each inline and the codebase fills with duplicates. The fix: treat
MODULE_REGISTRY.mdas a dedup ledger and the shared/feature trees as the source of truth — before writing any component, check whether it already exists; reuse it, extend it, or only then create it, and put reusable ones incomponents/shared/so the next section reuses them. Readreferences/dedup-protocol.md; it's the heart of phase 2.
Prerequisites & resolution
- Resolve the project dir. Read
.claude/workspace.json(repo root) →frontendentry =<proj>. Else find afrontend-*folder (e.g.frontend-shoply/), a plainfrontend/, or asrc/app/with shadcn (globals.css+components/ui/). If there's no scaffolded Next.js + shadcn project, stop and point atnextjs-bootstrap— this skill builds into an existing structure, it doesn't create one. - Read the contract files (required):
ARCHITECTURE.md(conventions) andMODULE_REGISTRY.md(the dedup ledger — every existing shared component, hook, util). You cannot dedup against a ledger you haven't read. - Resolve the HTML input. In priority order: an explicit path the user gave → a pasted HTML block in the message → a URL (fetch it) → scan
_docs/designs/(the project's design drop-folder) and use/confirm the relevant file. If the HTML links external CSS (<link rel="stylesheet">) or fonts, read those files too — the styling lives there. Seereferences/reading-html.md. - Determine the package manager (
pnpm/npm/yarnfrom the lockfile) for shadcn/build commands below.
Decide the scope (which phases to run)
- A whole page / a fresh design, or the project still has the stock shadcn theme → run Phase 1 (theme) then Phase 2 (components).
- A single section/snippet on an already-themed project → Phase 2 is the focus, but still scan the snippet for new tokens (a brand color or font not yet in the theme). Don't hardcode them — append them to the theme (a light Phase 1 pass) and then reference them. Surface to the user when you add a token.
- The user explicitly asks for only theming or only a component → do just that phase. Stay in scope.
State which phases you'll run before you start, so the user can redirect.
Phase 1 — Theme (tokens → globals.css + layout.tsx)
Skip or trim per the scope decision above. Paths are relative to
<proj>.
- Extract the tokens from the HTML. Pull every recurring color, font-family, font-size/line-height, font-weight, radius, shadow, spacing step, gradient, and background image out of the
<style>blocks, linked CSS, inlinestyle=attributes, and any baked-in Tailwind classes. Runscripts/extract_tokens.py <file>to get a frequency-sorted inventory fast, then read it with judgment — recurring values are tokens; one-offs belong to a component, not the theme. Seereferences/reading-html.md("Extracting design tokens"). - Normalize into a token map. Group into: colors (→ shadcn roles), typography (families + the size/line-height/weight scale), radius, shadows, spacing, gradients, background images. Map the design's colors onto shadcn's semantic roles rather than inventing names (so every shadcn component is themed for free). See
references/theme-tokens.md. - Convert every color to oklch. Collect the values into
{role: "#hex"}and runpython scripts/hex_to_oklch.py --json colors.json. Use the output verbatim — shadcn/Tailwind v4 store theme colors as oklch; never paste raw hex into theme variables. - Rewrite
globals.css— light AND dark. Update the:root(light),.dark(dark), and@theme inlineblocks — re-value the roles, don't rename them. shadcn themes are dual-mode: if the HTML defines a dark variant (.dark/[data-theme="dark"]block,prefers-color-scheme: darkmedia query, ordark:Tailwind variants) capture that palette into.dark; if it's single-mode, fill the given mode and derive the other (don't leave stale shadcn defaults) and say you derived it. Set--radius, add--shadow-*, gradient tokens, and any custom type-scale entries.references/theme-tokens.mdhas the exact block shapes and the dark-derivation rules. - Wire fonts in
layout.tsx. Google families vianext/font/google, custom/brand files vianext/font/local(drop files undersrc/app/fonts/). Each font exposes a CSS variable; add them to<html className>and bind them in@theme(--font-sans,--font-display, …). Never<link>tags or@import url(). Seereferences/fonts.md. - Apply the rest as tokens. Radius, shadows, gradients, background images → tokens/utilities so components reference
rounded-lg,shadow-card,bg-[image:var(--gradient-brand)], never raw values. - Update the contract files. In
ARCHITECTURE.md, make the theme concrete (fonts used, palette source = this HTML, oklch). InMODULE_REGISTRY.md, log the theming decision.
Phase 2 — Components (the dedup-first build)
Paths relative to
<proj>. Conventions come fromARCHITECTURE.md+ the references below; project docs win on project-specifics.
- Map the structure before coding. Read the HTML top-down. Semantic tags (
<header>,<nav>,<main>,<section>,<footer>,<aside>) and class names (hero,card,navbar,avatar) are your decomposition map and dedup hints — the HTML analog of Figma layer names. List the discrete pieces. For a big page, outline the sections first, then drill into each. Seereferences/reading-html.md. - Classify each candidate piece reusable-generic vs feature-specific using
references/shared-taxonomy.md. A block that is a header/navbar/hero/carousel/chart/sidebar/avatar/logo/banner is almost always a shared component. - Run the dedup protocol for every candidate (
references/dedup-protocol.md): checkMODULE_REGISTRY.md→ grepcomponents/shared,components/ui,features/*/components→ decide reuse / extend / create. Never create something the ledger already lists. If a feature-local component is now needed by a second feature, move it to shared (don't copy). The protocol covers non-component reusables too — utils, hooks, types, constants get the same search (lib,hooks,src/types,src/constants) and the same placement-by-dependency rule, so design-time code lands wheremodule-builderexpects it. - Fill primitive gaps via shadcn, not by hand. If a piece is really a primitive the project lacks (
carousel,avatar,chart,tabs,accordion,sidebar, …), add it with<pm> dlx shadcn@latest add <name>intocomponents/ui/and compose it — never hand-author or re-skin a primitive. - Build each new component — mobile-first and token-based. Tailwind utility classes only (no separate CSS files; no inline
style={{}}for things Tailwind covers); reference theme tokens (bg-primary,text-muted-foreground,rounded-lg,shadow-card,font-display) instead of the raw hex/px in the source HTML — never a hardcoded color when a token exists. Author mobile-first: base classes target small screens, layer up withmd:/lg:prefixes; honor the source's@mediaqueries via breakpoints, and infer the mobile layout when the markup is desktop-only. Any header/navbar/sidebar collapses on mobile into a hamburger that opens a shadcnSheet. Match spacing, gap, radius, border, shadow, line-height faithfully — through the token system, responsively (don't pin desktop px). TypeScript props;cvafor variant-bearing components. Reusable →components/shared/<group>/; domain-specific →features/<name>/components/. Seereferences/building-components.md. - Assets & icons. For
<img>/background images in the HTML: copy local asset files intopublic/(or the feature) and usenext/image; for remotesrcURLs, download intopublic/rather than hotlinking. Icons come from the single registry filecomponents/shared/icons.tsx(lucide-react first, react-icons for gaps, customcurrentColorSVGs last) — map source icon fonts/inline<svg>s to lucide equivalents and import from the registry, never from the icon library directly. Seereferences/building-components.md("Assets", "Icons"). - Animation. Default to Tailwind/CSS transitions and shadcn's built-in motion; reach for framer-motion (
motion/react) only for orchestrated/gesture/scroll-linked motion. Translate CSStransition/@keyframes/animationin the source to the equivalent. Seereferences/animation.md. - Compose & register. Assemble the pieces into the screen via a feature
template/if it's a full screen; keeppage.tsxthin. Register every new shared component inMODULE_REGISTRY.md(name, path, what it wraps, purpose) — an unregistered shared component is a future duplicate. - Verify.
<pm> run build(typecheck + lint) and a quick read-through against the source HTML. Report: theme changes (if Phase 1 ran), components created vs reused, where each landed, primitives added, registry rows added.
What to read when
references/reading-html.md— how to read the input (file / pasted / URL /_docs/designs), resolve linked CSS & fonts, parse structure for decomposition, and extract design tokens from plain CSS, inline styles, and baked-in Tailwind. Read before Phase 1 step 1 and Phase 2 step 1.references/theme-tokens.md— shadcn role table, color→role mapping, the exact Tailwind v4:root/.dark/@theme inlineblock shapes, and radius/shadow/gradient/bg-image tokens. Read during Phase 1 steps 2–6.references/fonts.md—next/font/googleandnext/font/localpatterns, exposing & binding CSS variables, handling the families the HTML uses. Read during Phase 1 step 5.references/dedup-protocol.md— the reuse/extend/create decision and the exact search order. Read before creating any component. This is the skill's reason to exist.references/shared-taxonomy.md— reusable-shared vs feature-specific, the canonical shared list (header, navbar, hero, small-hero, carousel, charts, sidebar, avatar, logo, banner …) and whichcomponents/shared/subfolder each lands in. Read during Phase 2 step 2.references/building-components.md— Tailwind-token styling rules,cva, server vs client, props/typing, faithful spacing/shadow/radius via tokens, assets/next/image, accessibility. Read before Phase 2 step 5.references/animation.md— Tailwind/CSS vs framer-motion decision and patterns; translating CSS animation from the source. Read before Phase 2 step 7 (only if the design animates).scripts/extract_tokens.py— scans an HTML/CSS file and prints a frequency-sorted inventory of colors, font-families, font-sizes, radii, shadows, gradients. Run it first in Phase 1.scripts/hex_to_oklch.py— hex/rgb →oklch()converter (single, many, or--jsonbatch). Use it for every color.
Non-negotiables (why this skill exists)
- Colors are oklch, tokens not raw values. Recurring colors become shadcn role tokens in
globals.css, converted with the script. A component shippingbg-[#3B82F6]ortext-[14px]when a token exists defeats the theme and the next redesign. Always use the token — translate raw HTML values tobg-primary/text-sm/rounded-xl/shadow-card, never a hardcoded color. - Mobile-first responsive, with a real mobile nav. Every component is authored mobile-first (base = small screen,
md:/lg:layer up), never desktop-pinned to the mockup's fixed width. Any header/navbar/sidebar collapses on mobile into a hamburger that opens a shadcnSheet— not links silentlyhiddenwith no affordance. - Check the registry before you create — every time. The
MODULE_REGISTRY.mdledger + a grep ofshared/ui/featuresis mandatory per candidate component. Re-creating something that exists is the exact failure this skill prevents. Reuse > extend > create. - One home per component, by dependency. Generic/reusable →
components/shared/<group>/. Depends on one feature's domain →features/<name>/components/. A reusable component buried in a feature folder is a bug; so is a domain component in shared. When a feature-local piece is needed by a second feature, it moves to shared and gets registered — never copied. The same placement-by-dependency applies to non-components: utils →lib/, hooks →hooks/, multi-feature types →src/types/, multi-feature constants →src/constants/. - shadcn primitives are composed, never duplicated. Need a carousel/avatar/chart/tabs/sidebar?
shadcn addit and wrap it. Hand-writing a primitive shadcn ships, or forking a styled sibling ofButton/Select, is a regression. When the design's primitive looks different, edit itscvavariants in place (seereferences/building-components.md"The design's variants live inui/") — that's the sanctioned path. - Don't restructure shadcn's theme — re-value it. Keep the role names and the
:root/.dark/@theme inlineshape; swap values and add tokens. Fonts go throughnext/font, never<link>/@import. - Always produce both light and dark. shadcn's theme is dual-mode. Capture the design's dark palette when it has one; derive it when it doesn't. Every role in
:rootmust also exist in.dark— a missing role is an unthemed component in dark mode. Leaving stock shadcn dark defaults under a custom light theme is the failure to avoid. - Reusable shared components get registered immediately. A new
Header/Avatar/Carouselincomponents/shared/that isn't inMODULE_REGISTRY.mdis invisible to the next run and will be rebuilt. - Faithful, but production-grade. Honor the design's spacing, gaps, radii, borders, shadows, gradients, line-heights — through tokens, responsively (mobile-first, not desktop-pinned). Semantic markup, alt text, focus states. The output should look like a senior engineer built it, not like converted markup.
- Stay in your lane. Build into the existing structure. Don't scaffold (that's
nextjs-bootstrap). When the source is Figma, usefigma-to-componentinstead.