| name | component-vrt |
| description | Focus visual regression testing on ONE component instead of a whole page, so the diff is small enough to read and to hand to a model. Mounts a story from a Playwright component-testing gallery and screenshots only that component (`vlmkit check story`), then loops baseline → diff → fix → re-run to green. Includes copyable gallery reference implementations (vanilla / React / Vue) because Playwright ships no template for the gallery. Use when repairing or restyling a specific component, when a full-page diff is too noisy or cascades, or when a project wants per-component VRT baselines. Not for whole-page comparison (use vrt-visual-diff) or for generating browser tests from a spec (use spec-to-playwright). |
| metadata | {"internal":true} |
component-vrt
Repairing one component with a full-page diff is the wrong instrument. The image
is large, the diff cascades — nudge a header and every row below it reports as
changed — and the part under repair is buried in the part that is not.
vlmkit check story mounts one story and screenshots only that component.
Measured on this skill's own fixture: 30,448px across three stories against
1,440,000px for the same count of full-viewport shots — 47x smaller — and a
story that did not change stays green when a shared stylesheet does.
When to use this instead of a page gate
| Situation | Use |
|---|
| Restyling / repairing one component | this skill |
| Component renders differently in two places on one page | vlmkit check drift component |
| Whole page vs whole page, two current renders | vrt-visual-diff |
| Natural-language spec → browser test | spec-to-playwright |
| Edited markup, no reference, want correctness gates | markup-assist |
Two cases where a story diff is the weaker instrument
Measured over 65 trials
(report).
Both are about check story being a pixel instrument, while diff html also
reads computed styles:
- A large component with the default threshold. On a 1258x203 hero, story
diffs missed 6 of 12 seeded changes where
diff html missed 0. A corner-radius
change moves ~60 pixels — over 1.6% of a button, 0.02% of that hero, under the
0.5% default. Get a per-story threshold from
vlmkit build gallery, or compute one; do not reuse the default on a big
component.
- Sub-perceptual style drift. A pale-palette shift moved 96% of that hero's
pixels by ≤8/255 per channel, which the comparator scores as 0% changed —
no threshold reaches that.
diff html catches it from the changed
background-image declaration. check story now reports it as
sub-perceptual-drift (a warn, so the verdict is unchanged) rather than
letting it read as a clean pass — but the comparator still passes it, so a
project that treats tint drift as a regression must promote the rule.
So story VRT narrows what a page diff has to catch; it does not replace it.
Keep a page-level diff html in the suite.
Prerequisite: does the project have a gallery?
A gallery is one page exposing window.mount({ story, props }) /
window.unmount() that renders into #root. It is the half of Playwright
component testing that lives in the user's repo, and Playwright's docs are
explicit that it is framework-specific with no template to copy.
Check in this order:
-
A gallery already exists — look for window.mount in the repo, or a
baseURL in playwright.config.* pointing at a gallery/ path. Use it.
-
No gallery — copy one from assets/. Do not write one from scratch;
the templates already handle the two things that break screenshots (awaiting
layout, freezing animation). Read
assets/_gallery-contract.md first, then:
| Stack | Copy |
|---|
| React | assets/gallery.react.template.tsx + assets/gallery.host.html |
| Vue | assets/gallery.vue.template.ts + assets/gallery.host.html |
| No framework / want zero setup | assets/gallery.vanilla.html (runs over file://) |
Drop .template from the filename and adjust the import.meta.glob to the
project's story layout. That glob is the only non-portable line.
-
No stories either — assets/button.story.template.tsx shows the shape,
including the hidden-form pattern for state.
-
The components are already built and rendered on a page, and you only want
VRT over them — generate the gallery from that page instead of writing one:
vlmkit build gallery dist/index.html --out .vlmkit/gallery
It captures each component's markup and the page's CSS, and prints the
check story commands with a per-story threshold derived from each
component's area. The tradeoff: captured markup is frozen, so props do
nothing and behaviour is not exercised. Take option 2 over this whenever the
component's states matter.
Do not add a Playwright version bump for this. The mount fixture is
1.62+, but check story does not use it — it drives the page-side contract via
page.evaluate, exactly as the fixture does. Only add
assets/playwright.ct.config.template.ts if the user also wants behavioural
component specs.
Storybook is not drop-in: its iframe renders from a URL query param and
exposes no window.mount, so check story reports mount-failed. A shim in
.storybook/preview.js would bridge it; that is unverified, so say so rather
than promising it.
The loop
G="http://localhost:5173/playwright/gallery/index.html"
vlmkit check story components/Button/Primary --gallery "$G"
vlmkit check story components/Button/Primary --gallery "$G"
vlmkit check story components/Button/Primary --gallery "$G" --update-baseline
Several stories share one browser — list them together rather than looping in the
shell:
vlmkit check story components/Button/Primary components/Button/Disabled components/Card/Default --gallery "$G"
Useful flags: --props '{"title":"Hello"}', --viewport 400x300,
--threshold 0.02, --root '#app', --settle 200, --out <dir>.
Machine-readable: --json gives the shared gate envelope, with region geometry
and shift estimates under findings[].evidence.regions — read that rather than
parsing the prose.
Reading the outcome
Three rules, and the difference between the first two decides what you do next.
| Rule | Means | Action |
|---|
story-drift | The component changed vs its baseline | The finding you asked for. Read regions, fix, or --update-baseline if intended. |
mount-failed | Nothing was measured — unknown story id, render throw, page is not a gallery | Fix the id or the gallery. Never --rule check.story/mount-failed=off to reach green: that makes a typo'd story id read as a passing component. |
sub-perceptual-drift | The story passed, but most of its pixels moved by less than the comparator counts — the signature of a palette / gradient / opacity change, not of antialiasing | Look at it. This is the one blind spot a story diff has that a page diff html does not (it reads computed styles). Promote to suspect in vlmkit.gates.json if your project treats tint drift as a regression. |
new-baseline | No baseline existed, so one was written | Re-run to actually compare. Commit the baseline. |
Done conditions
- Every targeted story reports
unchanged, or its change is approved with
--update-baseline and the new baseline is committed.
- No
mount-failed findings.
vlmkit check story <stories> --gallery <url> exits 0 without --advisory.
- Any story over ~50,000px carries a derived
--threshold rather than the
default, and a page-level diff html still covers what story diffs cannot see
(see above).
Persisting the set
Once the stories matter, list them in vlmkit.gates.json so CI runs the same set
and the story ids stop drifting:
{
"defaults": {
"gates": [
"check story components/Button/Primary --gallery http://localhost:5173/playwright/gallery/index.html",
"check story components/Card/Default --gallery http://localhost:5173/playwright/gallery/index.html"
],
"rules": { "check.story/new-baseline": "off" }
}
}
That config is also the fix for the one sharp edge: baselines are keyed on the
story id as written. The gallery owns id resolution and the contract offers no
way to ask what an id resolved to, so Button/Primary and
components/Button/Primary get separate baselines. Pick one spelling per story
and keep it.
Flake
A component-scoped shot removes most page-level flake by construction. What is
left, in order of how often it is the cause:
- Fonts. A fallback substituting mid-run reads as real drift. Self-host the
fonts the component uses.
- Animation. Freeze it in the gallery's
window.mount — the templates here
already do. --settle <ms> is the fallback, not the fix: waiting slows every
run and only reduces the flake.
- Threshold. Default is
0.005, deliberately tighter than a page default:
on a 3,520px button a handful of stray pixels is already a percent. Raise it
per-story with --threshold rather than globally.
Related
vlmkit rules check story — the rule table with default severities.
examples/story-gallery/
in the vlmkit repo — the vanilla gallery as a runnable project, plus a README
with the React + Vite version.