Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Clone any website into a pixel-perfect single-file HTML prototype. Extracts design tokens, assets, CSS computed styles, interaction patterns, and content via Playwright. Outputs a self-contained HTML file with real data injection. Use when the user wants to clone, replicate, reverse-engineer, or create a pixel-perfect copy of any website or web app. Provide one or more target URLs as arguments.
.claude/skills/coco-research-clone-website/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 203% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 132% | 0% |
You are about to reverse-engineer $ARGUMENTS into a pixel-perfect single-file HTML prototype.
This is adapted from the ai-website-cloner-template approach but optimized for rapid prototyping workflows: single self-contained HTML files (no build system, no CDN dependencies, opens directly in a browser).
Unlike the original repo (Next.js + shadcn/ui), our output is:
<style> + <body> + <script> sectionsdesign-system.json colocated with the projectnpx playwright --version. If not installed, ask the user to run npm i -D playwright.$ARGUMENTS as one or more URLs. Validate each is accessible.{project}/Screenshots-clone/ for captured assets.--extract): capture design tokens, screenshots, and component specs without building--sections "header,table,sidebar"): clone only named sectionsUse Playwright to capture:
javascriptimport { chromium } from 'playwright'; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); await page.goto(URL); // Full page screenshot await page.screenshot({ path: 'full-desktop.png', fullPage: true }); // Viewport screenshot await page.screenshot({ path: 'viewport-desktop.png' }); // Mobile await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: 'viewport-mobile.png', fullPage: true });
Run this in Playwright's page.evaluate() to extract the complete design system:
javascriptconst tokens = await page.evaluate(() => { // Colors const colorMap = new Map(); document.querySelectorAll('*').forEach(el => { const cs = getComputedStyle(el); ['color','backgroundColor','borderColor','borderTopColor','borderBottomColor'].forEach(p => { const v = cs[p]; if (v && v !== 'rgba(0, 0, 0, 0)' && v !== 'transparent') colorMap.set(v, (colorMap.get(v)||0)+1); }); }); // Typography const fontMap = new Map(); document.querySelectorAll('*').forEach(el => { const cs = getComputedStyle(el); const key = `${cs.fontFamily}|${cs.fontSize}|${cs.fontWeight}|${cs.lineHeight}`; fontMap.set(key, (fontMap.get(key)||0)+1); }); // Spacing const spacingSet = new Set(); document.querySelectorAll('*').forEach(el => { const cs = getComputedStyle(el); ['padding','margin','gap'].forEach(p => { const v = cs[p]; if (v && v !== '0px') spacingSet.add(v); }); }); // Shadows const shadowSet = new Set(); document.querySelectorAll('*').forEach(el => { const v = getComputedStyle(el).boxShadow; if (v && v !== 'none') shadowSet.add(v); }); // Radius const radiusSet = new Set(); document.querySelectorAll('*').forEach(el => { const v = getComputedStyle(el).borderRadius; if (v && v !== '0px') radiusSet.add(v); }); return { colors: [...colorMap.entries()].sort((a,b) => b[1]-a[1]).slice(0,30), typography: [...fontMap.entries()].sort((a,b) => b[1]-a[1]).slice(0,20), spacing: [...spacingSet].sort(), shadows: [...shadowSet], radii: [...radiusSet].sort() }; });
For each major component/section, extract exact computed styles:
javascript// Run per component container const componentCSS = await page.evaluate((selector) => { const el = document.querySelector(selector); if (!el) return null; const props = [ 'fontSize','fontWeight','fontFamily','lineHeight','letterSpacing','color', 'textTransform','textDecoration','backgroundColor','background', 'padding','paddingTop','paddingRight','paddingBottom','paddingLeft', 'margin','marginTop','marginRight','marginBottom','marginLeft', 'width','height','maxWidth','minWidth','display','flexDirection', 'justifyContent','alignItems','gap','gridTemplateColumns', 'borderRadius','border','boxShadow','overflow','position', 'top','right','bottom','left','zIndex','opacity','transform','transition' ]; function extract(element, depth) { if (depth > 4) return null; const cs = getComputedStyle(element); const styles = {}; props.forEach(p => { const v = cs[p]; if (v && v !== 'none' && v !== 'normal' && v !== 'auto' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') styles[p] = v; }); return { tag: element.tagName.toLowerCase(), classes: element.className?.toString().split(' ').slice(0,5).join(' '), text: element.childNodes.length === 1 && element.childNodes[0].nodeType === 3 ? element.textContent.trim().slice(0,200) : null, styles, children: [...element.children].slice(0,20).map(c => extract(c, depth+1)).filter(Boolean) }; } return extract(el, 0); }, selector);
Use Playwright to discover behaviors:
javascript// Scroll sweep for (let y = 0; y < await page.evaluate(() => document.body.scrollHeight); y += 300) { await page.evaluate(y => window.scrollTo(0, y), y); await page.waitForTimeout(200); // Check for sticky header changes, scroll-triggered animations, etc. } // Hover sweep --- hover over interactive elements const interactiveEls = await page.$$('button, a, [role="tab"], .nav-item, .card'); for (const el of interactiveEls.slice(0, 30)) { await el.hover(); await page.waitForTimeout(100); } // Click sweep --- test tabs, dropdowns, etc. const tabs = await page.$$('[role="tab"], .tab-item, .nav-tab'); for (const tab of tabs) { await tab.click(); await page.waitForTimeout(300); }
javascript// Extract all SVG icons as inline code const svgs = await page.evaluate(() => [...document.querySelectorAll('svg')].map(s => ({ html: s.outerHTML, parent: s.parentElement?.className?.toString().split(' ')[0] || 'unknown', width: s.getAttribute('width'), height: s.getAttribute('height') })) ); // Extract all image URLs for download const images = await page.evaluate(() => [...document.querySelectorAll('img')].map(i => ({ src: i.src, alt: i.alt, w: i.naturalWidth, h: i.naturalHeight })) );
Convert extracted tokens into CSS custom properties:
css:root { /* Colors --- from extraction, mapped to semantic names */ --color-bg: {extracted}; --color-text: {extracted}; --color-text-muted: {extracted}; --color-accent: {extracted}; --color-border: {extracted}; /* ... */ /* Typography */ --font-family: {extracted}; --font-size-body: {extracted}; /* ... */ /* Shadows */ --shadow-sm: {extracted}; --shadow-md: {extracted}; /* ... */ }
For each major section of the page, dispatch a builder agent:
Complexity budget: If a section spec exceeds ~150 lines, split it.
Merge all sections into one self-contained HTML file:
<script> blockUse Playwright to:
When a design-system.json exists in the project:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 41,544 | 16,380 | -61% | 1 | 1 | 0% | 8,254 | 2,780 | -66% | 0 | 0 | — |
case-02 | fail→fail | 40,279 | 57,054 | +42% | 1 | 1 | 0% | 8,247 | 2,827 | -66% | 0 | 0 | — |
case-03 | fail→fail | 18,501 | 12,963 | -30% | 1 | 1 | 0% | 2,760 | 2,918 | +6% | 0 | 0 | — |
case-04 | pass→pass | 17,823 | 10,348 | -42% | 1 | 1 | 0% | 2,220 | 3,997 | +80% | 0 | 0 | — |
case-05 | fail→pass | 15,029 | 10,621 | -29% | 1 | 1 | 0% | 1,487 | 3,344 | +125% | 0 | 0 | — |
case-06 | fail→fail | 20,600 | 17,365 | -16% | 1 | 1 | 0% | 2,199 | 4,364 | +98% | 0 | 0 | — |
case-07 | fail→fail | 14,436 | 11,497 | -20% | 1 | 1 | 0% | 1,513 | 4,546 | +200% | 0 | 0 | — |
case-08 | fail→pass | 10,321 | 11,320 | +10% | 1 | 1 | 0% | 1,458 | 4,423 | +203% | 0 | 0 | — |
case-09 | fail→fail | 17,932 | 16,368 | -9% | 1 | 1 | 0% | 2,015 | 4,304 | +114% | 0 | 0 | — |
case-10 | fail→fail | 13,173 | 8,016 | -39% | 1 | 1 | 0% | 1,262 | 3,902 | +209% | 0 | 0 | — |
case-11 | fail→pass | 20,019 | 11,705 | -42% | 1 | 1 | 0% | 2,285 | 3,998 | +75% | 0 | 0 | — |
case-12 | pass→pass | 18,968 | 9,674 | -49% | 1 | 1 | 0% | 2,065 | 3,234 | +57% | 0 | 0 | — |
case-13 | pass→pass | 11,741 | 20,759 | +77% | 1 | 1 | 0% | 2,116 | 3,641 | +72% | 0 | 0 | — |
case-14 | fail→fail | 14,984 | 8,724 | -42% | 1 | 1 | 0% | 1,530 | 3,697 | +142% | 0 | 0 | — |
case-15 | fail→fail | 11,346 | 12,029 | +6% | 1 | 1 | 0% | 1,805 | 4,531 | +151% | 0 | 0 | — |
case-16 | fail→pass | 17,005 | 12,884 | -24% | 1 | 1 | 0% | 1,942 | 3,934 | +103% | 0 | 0 | — |
case-17 | fail→fail | 19,163 | 17,308 | -10% | 1 | 1 | 0% | 2,315 | 4,691 | +103% | 0 | 0 | — |
case-18 | fail→pass | 14,953 | 4,293 | -71% | 1 | 1 | 0% | 1,372 | 3,189 | +132% | 0 | 0 | — |
case-19 | fail→pass | 9,219 | 10,681 | +16% | 1 | 1 | 0% | 1,441 | 3,365 | +134% | 0 | 0 | — |
case-20 | pass→pass | 25,125 | 22,890 | -9% | 1 | 1 | 0% | 3,387 | 6,224 | +84% | 0 | 0 | — |
case-21 | pass→pass | 21,449 | 20,833 | -3% | 1 | 1 | 0% | 2,852 | 5,333 | +87% | 0 | 0 | — |
case-22 | pass→pass | 18,951 | 16,855 | -11% | 1 | 1 | 0% | 2,770 | 5,035 | +82% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +27 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.