skills/development/front-end/website-theme-porter/SKILL.md
Ports the visual identity of any live website into a React plus Tailwind CSS project. Extracts colours, typography, spacing, component styles via browser automation, source inspection, curl, wget, DevTools. Writes structured documentation, applies findings as Tailwind v4 CSS tokens, then verifies by visually diffing the original site against the local build. Use when cloning a brand, replicating a design system, matching a reference site, migrating visual identity, copying a style guide, porting a theme from any live URL into a React codebase, matching a client reference design.
npx skillsauth add pantheon-org/tekhne website-theme-porterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Port the visual identity of any live website into a React + Tailwind CSS project. The workflow is four linear stages: Extract → Document → Apply → Verify.
.context/artifacts/ before touching a single source file.Apply this skill when you need to replicate a visual design from a live URL into a React + Tailwind codebase: brand migrations, client reference designs, design-system ports, or style-guide replication.
See the When NOT to Use This Skill section at the end of this file for login-walled sites, canvas-rendered UIs, obfuscated class names, and licence considerations.
All artifacts (screenshots, extracted JSON, theme docs) must be saved under:
.context/artifacts/<website-slug>/<YYYY-MM-DD>/
Where <website-slug> is a lowercase, hyphenated version of the target domain
(e.g. designandbloom-co-uk), and <YYYY-MM-DD> is the extraction date.
SLUG="designandbloom-co-uk"
DATE=$(date +%Y-%m-%d)
ARTIFACTS=".context/artifacts/${SLUG}/${DATE}"
mkdir -p "${ARTIFACTS}"
All screenshot paths, JSON dumps, and theme docs in the steps below use ${ARTIFACTS}/
as their base. Never save artifacts to docs/ or any other location.
Load each reference file only when its stage begins — do not pre-load all three at once.
| Stage | Load | Do NOT load |
|-------|------|-------------|
| Stage 1: Extract | references/extraction.md | tailwind-mapping.md, verification.md |
| Stage 2: Document | references/tailwind-mapping.md | extraction.md (done), verification.md |
| Stage 3: Apply | references/tailwind-mapping.md (if not cached) | extraction.md, verification.md |
| Stage 4: Verify | references/verification.md | extraction.md, tailwind-mapping.md |
Pull raw design tokens from the target website. Choose the method that fits your situation:
Can you run agent-browser OR mcp-playwright in this environment?
YES → use Method A (automated browser extraction) — most thorough
agent-browser: run CLI commands directly
mcp-playwright: use playwright_browser_* MCP tools
NO ↓
Is the site's HTML/CSS source accessible (View Source, curl, wget)?
YES → use Method B (source inspection) — good for SSR/static sites
NO ↓
Can you manually inspect the live site in browser DevTools?
YES → use Method C (manual inspection) — always possible, least automated
NO → unblock agent-browser or mcp-playwright access before continuing
For the full command equivalents table, see references/extraction.md.
Steps below use agent-browser syntax. If using mcp-playwright, translate each command using the equivalents table above. The JS payloads are identical — only the invocation differs.
agent-browser open <TARGET_URL> && agent-browser wait --load networkidle
agent-browser screenshot --full "${ARTIFACTS}/source-full.png"
agent-browser set viewport 375 812 && agent-browser screenshot --full "${ARTIFACTS}/source-mobile.png"
agent-browser set viewport 1280 720
agent-browser eval --stdin <<'JS'
JSON.stringify({
colors: (() => {
const s = getComputedStyle(document.documentElement);
const props = Array.from(document.styleSheets)
.flatMap(sheet => { try { return Array.from(sheet.cssRules); } catch { return []; } })
.filter(r => r.selectorText === ':root')
.flatMap(r => Array.from(r.style));
return props.filter(p => p.startsWith('--')).reduce((acc, p) => {
acc[p] = s.getPropertyValue(p).trim(); return acc;
}, {});
})(),
computed: (() => {
const el = document.body;
const s = getComputedStyle(el);
return {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
lineHeight: s.lineHeight,
backgroundColor: s.backgroundColor,
color: s.color,
};
})()
}, null, 2)
JS
Save JSON output to ${ARTIFACTS}/tokens-css-vars.json.
If the colors object is empty (site uses no CSS custom properties):
The site styles everything with hardcoded values. Switch to computed-only extraction:
agent-browser eval --stdin <<'JS'
const els = { body: document.body, h1: document.querySelector('h1'),
btn: document.querySelector('button,[class*=btn]'),
nav: document.querySelector('nav,header'),
card: document.querySelector('[class*=card],article') };
const out = {};
for (const [name, el] of Object.entries(els)) {
if (!el) continue;
const s = getComputedStyle(el);
out[name] = { bg: s.backgroundColor, color: s.color, font: s.fontFamily,
fontSize: s.fontSize, fontWeight: s.fontWeight, borderRadius: s.borderRadius,
border: s.border, boxShadow: s.boxShadow, padding: `${s.paddingTop} ${s.paddingRight}` };
}
JSON.stringify(out, null, 2)
JS
Save to ${ARTIFACTS}/tokens-computed.json. Manually assign tokens from these values in Stage 2.
Note in ${ARTIFACTS}/theme/overview.md: "source uses hardcoded values — no design token system detected".
If the target is a SPA (React, Next.js, Vue, etc.): Content may not be rendered on first paint. Add a scroll-and-wait step before extracting:
agent-browser eval 'window.scrollTo(0, document.body.scrollHeight)'
agent-browser wait --load networkidle
agent-browser wait --timeout 2000
agent-browser eval --stdin <<'JS'
JSON.stringify(
['h1','h2','h3','h4','p','a','button'].reduce((acc, tag) => {
const el = document.querySelector(tag);
if (!el) return acc;
const s = getComputedStyle(el);
acc[tag] = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight,
color: s.color,
letterSpacing: s.letterSpacing,
};
return acc;
}, {}),
null, 2)
JS
Save JSON output to ${ARTIFACTS}/tokens-typography.json.
agent-browser screenshot --full "${ARTIFACTS}/source-annotated.png"
See references/extraction.md for the full JS extraction toolkit, colour parsing
helpers, nav/card/footer extractors, and page-by-page capture patterns.
Use when agent-browser is unavailable or blocked. Fetch the raw HTML and linked
stylesheets, then grep for CSS custom properties and font declarations manually.
curl -sL <TARGET_URL> -o "${ARTIFACTS}/source.html"
grep -oP '(?<=href=")[^"]*\.css[^"]*' "${ARTIFACTS}/source.html"
curl -sL <CSS_URL> -o "${ARTIFACTS}/styles-main.css"
grep -oP '--[\w-]+\s*:\s*[^;]+' "${ARTIFACTS}/styles-main.css" > "${ARTIFACTS}/tokens-raw.txt"
grep -E '@font-face|fonts\.googleapis' "${ARTIFACTS}/styles-main.css" > "${ARTIFACTS}/fonts-raw.txt"
Parse ${ARTIFACTS}/tokens-raw.txt manually and map values to the token table in Stage 2.
Note in ${ARTIFACTS}/theme/overview.md: "extraction method: source inspection (curl)".
Use as a fallback or to supplement other methods.
<html> → Computed tab → filter for -- → copy all custom properties to ${ARTIFACTS}/tokens-manual.txt.css → copy stylesheet contentsreferences/extraction.md → paste output to ${ARTIFACTS}/tokens-computed.json${ARTIFACTS}/Note in ${ARTIFACTS}/theme/overview.md: "extraction method: manual DevTools inspection".
Write the findings into ${ARTIFACTS}/theme/ before touching any code.
mkdir -p "${ARTIFACTS}/theme"
| File | Contents |
|------|----------|
| ${ARTIFACTS}/theme/colours.md | All extracted colours with hex/HSL, semantic role, usage context |
| ${ARTIFACTS}/theme/typography.md | Font families, size scale, weight scale, line heights |
| ${ARTIFACTS}/theme/spacing.md | Padding/margin patterns, gap values, container widths |
| ${ARTIFACTS}/theme/components.md | Button, card, nav, footer styles — border-radius, shadows, borders |
| ${ARTIFACTS}/theme/overview.md | Summary, source URL, extraction date, method used, key decisions |
## Colours
| Token | Hex | HSL | Role |
|-------|-----|-----|------|
| --primary | #0D9488 | 174 90% 30% | CTA buttons, links |
| --background | #FFFFFF | 0 0% 100% | Page background |
| --foreground | #111827 | 221 39% 11% | Body text |
## Typography
**Primary font**: Inter, sans-serif
**Heading font**: same (or specify if different)
| Element | Size | Weight | Line Height |
|---------|------|--------|-------------|
| h1 | 2.25rem | 700 | 1.2 |
| h2 | 1.875rem | 600 | 1.3 |
| body | 1rem | 400 | 1.6 |
Use the Token Decision Tree in references/tailwind-mapping.md to assign ambiguous colours.
Translate the documented theme into Tailwind CSS variables and component styles.
src/index.css (Tailwind v4 pattern)@import "tailwindcss";
/* Semantic colour tokens */
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(221 39% 11%);
--primary: hsl(174 90% 30%);
--primary-foreground: hsl(0 0% 100%);
--muted: hsl(210 40% 96%);
--muted-foreground: hsl(215 16% 47%);
--border: hsl(214 32% 91%);
--radius: 0.5rem;
}
/* Map to Tailwind utilities */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}
@layer base {
body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
}
}
index.html<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
Replace hardcoded colours/fonts with semantic Tailwind tokens:
// Before
<button className="bg-[#0D9488] text-white px-4 py-2 rounded">
// After
<button className="bg-primary text-primary-foreground px-4 py-2 rounded-[var(--radius)]">
Work page-by-page, starting with shared layout components (Header, Footer) then pages.
Compare the original site visually against the local build.
bun run dev # or: npm run dev / vite
agent-browser produces a pixel-diff image and mismatch percentage:
agent-browser diff url <TARGET_URL> http://localhost:5173 --screenshot "${ARTIFACTS}/diff-homepage.png"
mcp-playwright — navigate to each URL and take screenshots without a filename; images are returned inline into context so the agent can inspect and describe differences directly:
playwright_browser_navigate(url: "<TARGET_URL>")
playwright_browser_wait_for(time: 2)
playwright_browser_take_screenshot(fullPage: true) # source — inline
playwright_browser_navigate(url: "http://localhost:5173")
playwright_browser_wait_for(time: 2)
playwright_browser_take_screenshot(fullPage: true) # local — inline
# agent-browser
agent-browser diff url <TARGET_URL> https://<your-preview>.vercel.app --screenshot "${ARTIFACTS}/diff-deployed.png"
# mcp-playwright: repeat the navigate + take_screenshot (no filename) pattern above against the preview URL
See references/verification.md for the full diff interpretation guide, threshold
table, and symptom→fix patterns for all common mismatches.
<canvas> element have no CSS to extract; getComputedStyle returns nothing useful._3xKy9 are generated at build time and change on every deploy; do not attempt to port them. Extract computed values only via Method A JS snippets.Prioritise in this order:
@font-face or Google Fonts links), size scale, weightsNEVER wrap HSL values in hsl() inside @theme inline.
WHY: In Tailwind v4, --color-primary: hsl(174 90% 31%) inside @theme inline will
double-wrap when Tailwind generates utilities, producing broken output.
Consequence: bg-primary resolves to hsl(hsl(174 90% 31%)) — an invalid value; the colour renders as transparent or black.
/* ❌ BAD — double-wraps at utility generation time */
@theme inline {
--color-primary: hsl(174 90% 31%);
}
/* ✅ GOOD — bare channels in @theme inline; hsl() in :root */
:root { --primary: hsl(174 90% 31%); }
@theme inline { --color-primary: var(--primary); }
NEVER extract colours from :hover pseudo-states.
getComputedStyle on a hovered element requires the pointer to be physically over it.
The value may silently return the resting state, giving a token that looks correct but is wrong.
Consequence: Hover colours get assigned as the base token — all buttons show the hover shade permanently.
NEVER use prefers-color-scheme media query colours as your base tokens.
Computed styles reflect the current OS colour scheme at extraction time.
Consequence: If the machine is in dark mode, extracted "background" is near-black — applying it to a light-mode project inverts the entire colour scheme.
Always extract in light mode, or explicitly document which mode was active in overview.md.
NEVER use /tmp for any artifact storage.
/tmp is ephemeral and session-scoped — artifacts saved there are lost when the session ends.
Always save to .context/artifacts/<website-slug>/<YYYY-MM-DD>/.
# ❌ BAD
agent-browser screenshot /tmp/source.png
# ✅ GOOD
agent-browser screenshot "${ARTIFACTS}/source-full.png"
NEVER copy the source site's actual CSS files or stylesheets into your project. Rebuild every value from scratch using the extracted numbers.
NEVER use arbitrary Tailwind values (bg-[#abc]) for semantic colours.
Arbitrary values bypass the token system — use semantic utility classes instead.
// ❌ BAD — hardcoded, bypasses token system
<button className="bg-[#0D9488] text-white">
// ✅ GOOD — semantic token, single source of truth
<button className="bg-primary text-primary-foreground">
tools
A skill that produces warnings but no errors.
testing
A well-formed example skill for testing the validator.
development
A skill with code blocks and imperative instructions for testing content and contamination analysis.
tools