# Mirrorframe – clone a web page into React, verified per-node by pixel diff

> Source: <https://github.com/byte271/mirrorframe>
> Published: 2026-07-08 20:44:12+00:00

Clone a live web page into a **verified, self-contained React app** — capture it with a
headless browser, distill a **Design Genome** (tokens + motion + interaction state
machines), reconstruct it deterministically, and prove the result **per node and per
interaction state** by visual diff. Built entirely on technology that exists today
(Playwright, WAAPI, MutationObserver, esbuild, pixelmatch). No LLM in the loop: every
output is inspectable and reproducible.

**This project is also packaged as a Claude Skill** — an AI agent should start from
[ SKILL.md](/byte271/mirrorframe/blob/main/SKILL.md) and the deeper docs in

[. This README is the human-facing overview.](/byte271/mirrorframe/blob/main/references)

`references/`

Why 0.x:the CLI flags, genome schema, and report format may still change. 0.x is a statement of API instability, not of dishonest numbers — every number below is reproducible from a clean clone.

Requires Node.js ≥ 18. No other machine state is assumed.

```
git clone <this-repo> && cd mirrorframe
npm install            # postinstall downloads the Playwright Chromium binary

# clone a live URL (only pages you own or that are permissively licensed):
node scripts/mirrorframe.js run --url https://your-site.example --out ./mf-out/yours

# or a local page directory containing an index.html (served automatically):
node scripts/mirrorframe.js run --dir ./fixtures/tabs --out ./mf-out/tabs

# optional capture viewport: --width 1280 --height 800   (defaults shown)

npm run verify         # regression gate: full pipeline on all 12 bundled fixtures
```

Open the reconstruction at `<out>/recon-app/index.html`

— a self-contained React app
(`app.jsx`

, `styles.css`

, `bundle.js`

) with all assets bundled locally in `<out>/assets/`

.

**Capture**→`capture.json`

: headless Chromium walks the DOM recording computed styles per node, plus every motion/interaction signal listed below, and downloads all referenced assets.**Genome**→`genome.json`

: clusters design tokens, compiles motion DNA and interaction DNA (recovered state machines), annotates the structure.**Reconstruct**→`recon-app/`

: deterministic React + CSS codegen with the recovered state machines re-wired as real listeners/observers/animations.**Converge**→`convergence.json`

+`summary.json`

: re-renders the reconstruction at the identical viewport, verifies per node and per state, auto-corrects residual drift, and replays every recovered interaction and scroll state.

- Full computed-style capture per node (layout, color, type, borders, shadows, radii, filters, transforms) at a configurable viewport
**Images**(`<img>`

) and**CSS background-images** downloaded and rewritten to local bundled copies (8MB/asset, 192MB/capture budgets; misses reported, never hidden)**Web fonts**: every`@font-face`

is downloaded, bundled, and re-declared locally**SVG passthrough**: inline SVG markup is preserved verbatim(new in 0.2): source + poster bundled locally (64MB/asset budget), playback attributes (autoplay/loop/muted/playsinline/controls) preserved, real`<video>`

`<video>`

re-emitted; degrades to poster/first-frame still if the source is unfetchable — never an empty box(new in 0.2): the currently painted frame is snapshotted and re-emitted as a pixel-true still`<canvas>`

- Same-origin iframes captured; cross-origin content masked with a fixed reason

- CSS
`@keyframes`

animations and transitions (recovered via WAAPI`getAnimations()`

) `:hover`

rules recovered verbatim from same-origin stylesheets;**JS-driven hover effects** recovered by a synthetic hover probe**Pointer choreography**(new in 0.3): mouse-movement effects are recovered by sampling the page over a 3×3 pointer grid and fitting every transform-matrix component as a plane over the pointer (least squares, R² ≥ 0.85) — parallax layers, 3D tilt cards (`matrix3d`

), magnetic offsets, pointer-linked scale.**Smoothing/lag is recovered too**: the exponential time constant of the "ultra-smooth" trailing chase is measured from a pointer-jump step response and replayed with the same lerp in the reconstruction. Non-planar pointer physics is reported as unclassified, never guessed**Scroll choreography**: scroll-position–linked style tracks are sampled at multiple scroll depths and re-emitted; verified by scroll-state replay at 25/50/75%**Sequential scroll-reveal stagger**(new in 0.3): reveal mutations are timestamped, so elements that fade/slide in one by one are replayed with their observed per-element offsets; each participant's own computed`transition`

(duration, easing curve,`transition-delay`

) is re-emitted verbatim — CSS`nth-child`

delay staggers and JS-timer staggers both survive the clone**Chronograph frame sampler**(new in 0.2): time-driven motion with*no declared animation object*(rAF loops, JS tickers) is detected and recorded at**every animation frame** for a bounded window (2400ms, ≤120 tracks) — transform, opacity, filter, clip-path per frame — then replayed verbatim as looping WAAPI animations built from the sampled offsets

Recovered from the page by synthetic probes — never assumed or hardcoded:

`reveal`

— IntersectionObserver scroll-reveal (hidden → visible class machines)`toggle`

— second click reverses the first (accordions, expand/collapse)`exclusive`

— one class moves between siblings (tabs, segmented controls)`pair`

— one trigger adds the class, another removes it (modal open/close, including triggers only reachable*inside*the open state)- Ambient (timer-driven) mutations such as autoplay carousels are detected during an input-free watch and excluded from learning rather than mislearned
- A probe that crashes on a page-script conflict records
`probe-error`

for that candidate and the sweep continues (new in 0.2)

**Per-node**: every tracked node gets its own similarity score and one status —`pass`

/`style-verified`

/`corrected`

/`failed`

/`animated-unstable`

/`hidden-at-capture`

/`time-varying-replicated`

(new in 0.2) /`skipped(reason)`

**Residual auto-correction**: failing nodes get their reconstructed computed styles diffed against capture, patched with a per-node override, and re-verified once**Layout convergence**: rendered rects measured against captured rects; drifting nodes get authored sizes baked in bounded passes** Behavioral replay**: every recovered interaction state is replayed with real clicks and pixel-diffed against a ground-truth screenshot of the original**Scroll replay**: the page is verified at multiple scroll depths, not just the fold** Pointer replay**(new in 0.3): ground-truth screenshots at fixed pointer checkpoints are re-shot on the reconstruction with the same real mouse moves (settle time scaled to the largest recovered smoothing tau) and pixel-diffed; replay shots are settle-aware, waiting until consecutive frames match so staggered reveals and lag-smoothed effects finish before the diff**Time-varying honesty**(new in 0.2): bundled video/canvas and frame-sampled nodes have their*mechanism*replicated, but pixels depend on the playback instant — they are masked from diffs with the fixed reason`time-varying-media`

, itemized, never averaged in or hidden**Fixed skip-reason taxonomy**(`scripts/lib/reasons.js`

, 10 reasons): every skipped node/probe/page carries exactly one reason; skipped elements keep their layout footprint via placeholders and are masked from the diff- Bounded everything: navigation fallback, probe budgets, node/candidate caps, 300s pipeline watchdog with distinct exit codes (0 ok / 3 page skipped / 4 crash)

All 12 bundled fixtures pass with **0 failed nodes, 0 failed states, 0 failed
scroll states, and 0 failed pointer states** — including `stress-stagger`

(CSS-delay + JS-timer sequential reveals, new in 0.3), `stress-pointer`

(parallax/tilt/smooth-follower pointer choreography, new in 0.3), `stress-frames`

(rAF inline-style motion + animated canvas), `stress-scale`

(230 nodes / 120
interactive), `stress-edgecss`

(container queries, `:has()`

), `stress-iframe`

,
`stress-flaky`

(404/hung assets), and `stress-carousel`

(autoplay → excluded,
not mislearned).

The pipeline has also been spot-validated against complex real-world production pages (hundreds of nodes; heavy video, scroll choreography, and rAF motion) with aggregate similarities ≥ 99.9%, all scroll states passing, media and web fonts fully bundled, and zero crashes — any sub-threshold node is itemized in the per-node report rather than averaged away. Nothing from third-party pages is bundled in this repo.

Success = zero `failed`

nodes and zero failed states. `style-verified`

,
`animated-unstable`

, `hidden-at-capture`

, `time-varying-replicated`

, and
`skipped(reason)`

are acceptable statuses when explained — see `SKILL.md`

for the
full interpretation guide. The one-screen summary is `summary.json`

; the full
per-node table is `convergence.json`

; residual maps are `diff-*.png`

.

| File | Meaning |
|---|---|
`capture.json` |
evidence bundle: node tree, computed styles, animations, scroll/frame tracks, probes |
`genome.json` |
Design Genome: tokens, motion DNA, interaction DNA, annotated structure |
`assets/` |
locally bundled images, fonts, videos, posters, media stills |
`recon-app/` |
generated React app (jsx + css + bundle + html) |
`original-*.png` / `state-*.png` / `scroll-*.png` / `pointer-*.png` |
ground-truth screenshots |
`recon-*.png` / `diff-*.png` |
reconstruction screenshots + pixelmatch residual maps |
`convergence.json` |
per-node table, correction log, state/scroll replay results, aggregates |
`summary.json` |
one-screen quantitative summary |

```
SKILL.md                        skill entrypoint: trigger description + agent workflow
scripts/mirrorframe.js          CLI: capture → genome → reconstruct → converge
scripts/verify-all.js           release verification: all fixtures + results table
scripts/batch-verify.js         batch validation against a list of live URLs
scripts/lib/props.js            shared tracked-property list + color normalization
scripts/lib/reasons.js          fixed skip-reason taxonomy (10 reasons)
scripts/lib/assets.js           asset downloader/bundler (images, fonts, video)
scripts/lib/capture.js          capture substrate: DOM/style walk, motion + interaction probes
scripts/lib/genome.js           genome compiler: tokens, tracks, behavior classification
scripts/lib/reconstruct.js      deterministic React + CSS codegen (state machines re-wired)
scripts/lib/converge.js         per-node + aggregate diff, auto-correction, state replay
references/behavior-patterns.md recovered-pattern catalog (reveal/toggle/exclusive/pair)
references/limitations.md       hard scope limits + expected non-pass statuses
references/roadmap.md           forward planning, feasible-now vs research-level
fixtures/                       12 self-authored test pages (4 core + 8 stress)
```

- Canvas/WebGL
*draw streams*are not reconstructed — a canvas becomes a pixel-true still of its captured frame, not a live re-render - Pointer physics beyond a planar matrix-component model (proximity-gated magnets, springs with overshoot) is reported as unclassified, not approximated; recovered smoothing is a single exponential time constant per node
- No general physics/inertia/spring recovery; frame tracks replay observed motion verbatim
- Behavior graphing covers class-toggle machines driven by click/scroll/hover — no drags, keyboard machines, or DOM insertion/removal machines
- One output target: React + plain CSS
- Responsive breakpoints are not re-captured at multiple viewports (single-viewport clone)
- "Any website" is explicitly not claimed — read the per-node report before trusting output

MIT (see `LICENSE`

). All fixtures are authored for this repo (system font stacks, no
external assets). Runtime dependencies: playwright (Apache-2.0), esbuild (MIT),
pixelmatch (ISC), pngjs (MIT), react / react-dom (MIT) — all permissive.

Only run against pages you own or that are permissively licensed; cloning third-party commercial sites raises IP/ToS issues this tool does not resolve for you.

- Recover constraints, not pixels: fixed
`width`

/`height`

are never baked into base rules — bold text is*allowed*to widen a tab (layout convergence bakes sizes only for measured drift, as a reported correction). - Recover state machines, not frames — except where frames
*are*the ground truth: the Chronograph tier replays sampled per-frame motion verbatim and says so. - Every claim in this README maps to a check in
`npm run verify`

; anything that failed or could not be observed is reported per node in`convergence.json`

rather than averaged away.
