StyleProof's job is a deterministic way to see visual regressions on the frontend. Here's what a report looks like.
StyleProof opens the app states you care about in a real browser, compares the base and head by computed CSS, and posts the evidence to the pull request. Intentional changes get approved. Unexpected changes block.
On a pull request, the PR comment is the same linked summary for public and private repositories.
Reports publish as workflow artifacts by default — the comment links the artifact on the run, and nothing is written to repository git history. report-storage: branch opts back into the styleproof-reports orphan branch for an in-browser rendered report.
Crops travel inside the published report package instead of being duplicated into the comment.
Private-repository viewers need repository access and an authenticated GitHub session.
If publication or receipt verification fails, StyleProof posts no delivery claim for that run.
The README can carry the crops directly. The block below is a real run, not a mockup:
scripts/live-readme-report.mjs captures example/demo/index.html in Chromium twice, injecting a
fixed CSS change between the captures, then renders the report and the PR comment it would post. The
block is that comment, with three edits for this page: crop links repointed at
docs/readme/live-report/crops/, element-level sections reordered so Save at rest comes before the
Docs hover, focus, and active states, and report.json left out of the committed bundle. Every
finding, value, and crop is what the run produced. Both sides of each interaction-state crop are in
that state.
2 computed-style difference(s) · 3 state-delta difference(s) across 1 distinct change(s) in 1 changed surface base with an existing baseline.
Surface base = one product UI state; capture keys with @width or live-state/popup variants are width or state captures of that base.
demo-button @ 900
padding 14px 28px → 18px 32px
background-color #14b8a6 → #dc2626
◀ before · after ▶ — demo-button @ 900
🔍 magenta boxes mark each change — changed: button.btn
button.btn
Style:
| Property | Before | After |
|---|---|---|
padding |
14px 28px |
18px 32px |
background-color |
#14b8a6 |
#dc2626 |
demo-button @ 900
Both sides are :hover. Left is the old :hover. Right is the new :hover.
:hover color #a5f3fc → #fca5a5
◀ base :hover · head :hover ▶ — both sides are :hover
🔍 magenta boxes mark each change — changed: a.link
a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:hover |
color |
#a5f3fc → #fca5a5 |
demo-button @ 900
Both sides are :focus. Left is the old :focus. Right is the new :focus.
:focus outline-color #5eead4 → #fca5a5
◀ base :focus · head :focus ▶ — both sides are :focus
🔍 magenta boxes mark each change — changed: a.link
a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:focus |
outline-color |
#5eead4 → #fca5a5 |
demo-button @ 900
Both sides are :active. Left is the old :active. Right is the new :active.
:active color #2dd4bf → #f87171
◀ base :active · head :active ▶ — both sides are :active
🔍 magenta boxes mark each change — changed: a.link
a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:active |
color |
#2dd4bf → #f87171 |
- Approve all changes
Tick Approve all changes to turn the StyleProof check green — write access required, and not the pull request author. One tick signs it off. A new push that changes styles or surfaces re-opens it.
Quickstart · Read the catch contract
Use StyleProof when a PR can change CSS, design tokens, component classes, layout, or hidden/open UI states and you want CI to say whether the browser's rendered styles actually changed. Unit and e2e tests prove behavior; StyleProof proves the visual contract for the states you declared.
It catches:
- a button recoloured by a token, utility class, CSS module, inline style, or design-system change;
- a layout shift at one breakpoint but not another;
- a dropped
:hover,:focus, or:activestyle; - a modal, menu, listbox, popover, sheet, or toast whose open state changed;
- a supposedly no-op refactor, such as CSS-to-Tailwind, that changed rendered output;
- a restyle on an element the PR also re-nested (a wrapper added or removed), paired back by geometry so the structural churn cannot hide it;
- a required route, component, or UI state that exists but has no capture.
The end-to-end catch contract lives in docs/what-it-catches.md.
- A surface is one UI state to certify: a route, tab, modal-open state, dropdown-open state, toast-visible state, loading state, etc.
- You list or auto-discover surfaces in a Playwright-style spec.
- StyleProof opens each surface at real breakpoint widths and records computed styles for every captured element.
- On a PR, it compares base vs head and reports exactly which rendered styles changed.
- The PR gets a
StyleProofstatus: green when nothing changed, red until someone approves intentional changes, or failing when certification mode is configured.
StyleProof is not a screenshot diff. Screenshots appear in the report so humans
can see the change, but the gate compares browser-computed CSS: resolved
longhands, pseudo-elements, layout boxes, motion longhands, and forced
:hover/:focus/:active deltas.
Version 6.4.0 removed the Phase 0 and Release Confidence public interfaces. Version 7.0.0 establishes the SemVer-correct major line for that contract. Upgrade the npm package and report Action to major 7 together, then refresh and verify generated workflows with the same server/spec options you used at install:
npm install --save-dev styleproof@^7
npx styleproof-init --upgrade
npx styleproof-init --checkRegenerate baselines so base and head use the same expanded forced-state evidence.
Remove integrations with the deleted phase0* / releaseConfidence* exports,
report.json.releaseConfidence, styleproof-release-confidence.json,
styleproof-publish-report --manifest-digest, and the Action's
release-confidence-digest output. Source binding, coverage, determinism,
product-state comparability, and all other retained gates still fail closed. See the
7.0.0 changelog.
npx styleproof setupThat one command detects npm, pnpm, Yarn, or Bun; installs StyleProof and
Playwright; installs Chromium; scaffolds the capture spec, a dedicated
production-build Playwright config, and one GitHub workflow; then verifies
every machine-owned file against the installed release. The default scaffold is
the smallest honest gate: one pull_request job captures base and head in the
same run and diffs them — maps live as workflow artifacts, no map-store branch,
no pre-push hook — and the gate runs advisory: it reports evidence on every
PR without blocking. Preview the exact operations without writing with
npx styleproof setup --dry-run. In a monorepo, target the consumer
application explicitly:
styleproof setup --project-dir apps/web--project-dir changes where dependencies are installed and all setup commands
run. --dir remains the capture-spec path inside that project, for example
--project-dir apps/web --dir e2e/styleproof.custom.spec.ts.
Opt-in flags reproduce the heavier architectures:
--workflow split— a read-onlypull_requestcapture job plus a trustedworkflow_runreport job. Required when the repository accepts fork or Dependabot PRs, whose tokens are read-only.--storage branch— the SHA-keyedstyleproof-mapsstore branch plus the pre-push publish hook, so CI restores maps instead of recapturing them.--mode certify/--mode review-gate— fail on any diff, or hold theStyleProofstatus red until a reviewer approves (scaffolds the approval workflow).split + branch + review-gatereproduces the pre-v7 scaffold exactly.
Existing scaffolds keep their architecture: a marker in the generated workflow
records the chosen mode, and styleproof-init --check / --upgrade verify
that mode's file set instead of rewriting it.
styleproof capture # capture this commit from the generated spec
styleproof crawl <url> # direct URL or rendered-nav crawl
styleproof compare [base] # fail-closed base/head comparison
styleproof report [base] # generate the review report on command
styleproof variants # inspect surface/state variants
styleproof affected # resolve surfaces affected by source changes
styleproof ci # CI orchestration: restore probes, cold captureRun styleproof --help for the whole journey or styleproof <command> --help
for command-specific options. The existing styleproof-* binaries remain as
backwards-compatible aliases for one major version.
npx styleproof capture # capture this commit's computed styles
npx styleproof compare # compare against the base branchcapture writes the current commit's map under .styleproof/maps/current
with a manifest; compare restores base and head and fails closed when the
evidence cannot certify. Maps travel as workflow artifacts by default —
never committed to the PR branch — or via the opt-in styleproof-maps store
branch. Nothing under .styleproof/ belongs in a commit.
For the hand-wired Action form, the fork/Dependabot split, the map-store and report branches, the pre-push hook, and every config key and flag, see docs/REFERENCE.md.
The scaffold's default gate is advisory — the evidence posts on every PR without blocking. These are the two gate modes you opt into:
Review-gate mode (require-approval: true) is for normal feature work:
every visual change is reported with evidence, the StyleProof status stays red
until a reviewer ticks Approve all changes, and approved changes become the
new baseline on merge. A surface that exists only on the PR head is still
reviewable: it holds the status red until approved, then becomes part of the
baseline once merged.
Certify mode (fail-on-diff: true) proves a change touched nothing
visual: any difference at all fails the job. It's the job StyleProof was born
for — certifying a CSS-to-Tailwind migration rendered byte-for-byte identical.
Reach for it on any change whose whole promise is "the output is unchanged": a
utility-class migration, a design-system swap, a dependency or build-tooling
bump. Zero diff is the contract; one drifting longhand is a regression to
investigate, not a change to approve.
There's also a third, spec-less use — pointing the one-shot capture at a design mockup and diffing your build against it until the number hits zero. See Match a design pixel-for-pixel.
A passing check is more than "no style changed". Four gates qualify it, and the report leads with their verdicts:
- Coverage — the
expectedregistry travels with the captured bundle as a ledger, so the gate states its completeness basis:✓ coverage complete,✗ coverage INCOMPLETE(blocks — a registered surface wasn't captured, even on an empty diff), or✗ completeness NOT asserted(no registry / filtered capture — blocks certification unless--allow-unasserteddiagnostic mode). - Determinism — the ledger records how each capture proved itself
(
self-checked/replayed); a green from anunprovenor unknown capture blocks, because a clean diff of two nondeterministic (or pre-ledger) reads could just be luck. Pass--allow-unassertedonly for explicit diagnostic compares (certifiesFully: falsein JSON). - Inventory — with
inventory: true(on instyleproof-initscaffolds), each capture harvests the surface's navigable affordances (links, tabs, menu items, keyed by stable identity, not label text). A removal that makes a feature unreachable gates — in thestyleproof-diffCLI and in the Action, both modes — until acknowledged instyleproof.inventory.json({"<key>": "<why>"}); a stale acknowledgement is flagged so the ledger can't rot. Details and the keying rules: docs/inventory-guard.md. Make it advisory in the Action with"gateInventoryRemovals": falseinstyleproof.config.json. Withoutinventory: truethe check does not run, and StyleProof says so: the report reads⚠ not checkedrather than✓ navigable set unchanged, and the Action warns that the gate could not run. An empty inventory diff is indistinguishable from "nothing was removed", so a ✓ there would claim a guarantee the run never earned. - Failed data request: a data-boundary request that failed during capture
means the fallback UI was captured, not the state its responses drive.
Gating is the default (
dataResidue: 'gate'): an unacknowledged failing endpoint blocks until declared instyleproof.data-residue.json, and a stale declaration also fails. Opt down withdataResidue: 'warn'. See Failed data request. - Product-state identity — a pair with matching
productState {id, revision}is comparable and can certify. Undeclared legacy pairs stay on the visual-review path until you arm the declare gate: addstyleproof.product-state.json({"<surface>": "<why>"}), pass--legacy-pairs, or setproductState.requireIdentity: true/--require-state-identity. StyleProof's own live dogfood arms this ledger so undeclaredhome@*pairs cannot stay green. Armed and undeclared → fail closed (CERTIFICATION_FAILED) in the CLI, Action verdict, and PR comment. Armed and declared → explicit advisory, never a certified green. Details: docs/product-state-comparability.md.
Those verdicts roll up into one more line the report always states: the
confidence ledger (styleproof-confidence.json, bundled next to the maps).
It assigns every surface one status — captured, excluded-with-reason,
inaccessible (an auth wall or blocked continuation), unknown (declared but
never captured), or unproven-determinism — and renders a completeness badge
(✓ complete, ⚠ limited, ⚠ unasserted, ⚠ unknown) separate from the
visual verdict: a visual PASS and a complete capture are two claims, never one
green. Crawl captures persist the ledger themselves (auth walls travel with the
bundle); spec captures derive it from the coverage ledger; bundles from before
the ledger existed read ⚠ unknown and are never blocked retroactively. No
coverage percentage is ever invented for surfaces that cannot be enumerated.
The same summary lands machine-readably in report.json (confidence).
StyleProof certifies only what it captured: a surface that was never captured
has no base and no head map, so its change can never appear in a diff and the
gate stays green having never looked at it. The coverage guard
(expected/exclude), the confidence ledger, the inventory guard, the
data-residue gate, and product-state identity exist to make every one of those
gaps loud instead of silent — full rules in
docs/REFERENCE.md.
The sharpest form of the boundary: maps prove only the states the spec exercises. A restyle confined to a conditional render branch the fixture never drove — a fault overlay, an empty state — produces byte-identical honest captures on both sides. Every conditional branch whose styling matters needs a surface that exercises it; see the un-exercised-state gap and what the crawler can and cannot reach.
Every flag, config key, Action input, workflow file, hook behavior, exit code, and optional layer lives in docs/REFERENCE.md. Deeper contracts: what it catches · component manifests · inventory guard · product-state comparability · report delivery · setup server contract · forced-state limits.
Repository CI runs the full browser suite in two file-level shards alongside the
Node 18/20/22 unit matrix. The e2e (node 22) aggregate compares completed test IDs
against a separately collected full inventory and requires the five-run determinism
receipt. Missing, duplicated, skipped, failed, or retried tests fail verification.
The browser-evidence-node-22 artifact retains the inventory, shard results, and
oracle receipt for 30 days. Local npm run test:e2e still runs the complete suite.
See CONTRIBUTING
for the dev loop, and AGENTS.md
(the same file as CLAUDE.md) for the operating rules and agent tooling. The repo
is wired for Claude Code with Ponytail (default lazy-coding mode), GitNexus
(code-intelligence graph — MCP server in .mcp.json, skills in
.claude/skills/gitnexus/), and Graphify (/graphify knowledge graph). The
GitNexus index (.gitnexus/) and Graphify output (graphify-out/) are gitignored;
build the index with npx gitnexus analyze.
MIT © Ben Sheridan-Edwards







