You shipped a design system so the buttons would stop being fourteen slightly different buttons. Tokens for color, spacing, and type. A component library. A reference site with every variant documented. For a while it held.
Then it drifted. Someone hardcoded a hex value under deadline. An AI coding assistant generated a card that looks almost like your card. A padding value crept from 16px to 12px on one template and nobody noticed. None of it broke a test, because none of your tests were watching the thing that changed — what the app actually looks like. The documented system says one thing; production quietly says another.
The zeroheight Design Systems Report 2026 puts numbers on how common this is: only 8% of teams call their design system very stable, while 44% report instability. Drift isn't an edge case. It's the default state of a design system that isn't actively defended. And the defense that works is the same one you already use for behavior — a regression suite, aimed at your visual language instead of your logic.
Drift has classes, and they fail differently
"The UI looks off" isn't a bug report you can act on. Drift shows up in recognizable classes, and naming them tells you what to watch.
Token drift. The system defines color-danger and space-4. Somewhere in the codebase a developer writes #e5484d and 12px directly, bypassing the token. It renders fine today. Then the token updates in a theme refresh, every proper consumer moves, and the hardcoded copy stays behind — now visibly wrong, and invisible to grep because it's just a number in a stylesheet. Token drift is the erosion of the single source of truth, one literal at a time.
Component variant drift. This is the one accelerating in 2026, and AI-generated code is a major source. An assistant is asked for "a pricing card," and it produces something structurally reasonable that ignores your library — slightly different border radius, its own shadow, a font weight that isn't in your scale. It passes review because it looks like a card at a glance. Multiply that across a team shipping AI-authored UI daily and your component library stops being the definition of your components and becomes one option among many.
Spacing and typography erosion. The quietest class. No single change is wrong enough to catch. A line-height nudged here, a margin collapsed there, a heading that now wraps to two lines on the template nobody opens. Each diff is defensible in isolation. The cumulative result is a product that no longer feels like it was built by one team — and there's no single commit to revert, because the damage is spread across fifty of them.
What these share: they're all invisible to functional tests. A test that clicks "Subscribe" and asserts the plan activated passes whether the button is your button or a drifted impostor. As the visual-vs-functional breakdown puts it, functional coverage is deliberately blind to appearance — that blindness keeps it stable across redesigns, and it's exactly why it can't see drift.
Pin the stable surfaces, and only those
Here's where teams get visual regression wrong, and why so many rip it out after a month: they snapshot everything. Every page, full-frame, on every run. Then the suite fails constantly — a timestamp moved, a promo rotated, an A/B variant flipped, a font rendered one antialiased pixel differently on the CI box than on the machine that made the baseline. The signal drowns. People start hitting re-run until it's green, and once that habit sets in the visual suite means nothing — the same trust collapse that kills flaky functional suites.
Visual regression earns its keep on stable, high-value surfaces and nowhere else. For defending a design system, that's a short, deliberate list:
- The reference pages. The component gallery, the storybook-style showcase, the living style guide. These pages exist to render every component in every state. They should be pixel-stable by definition — if one moves, either it's an intentional system change or it's drift. There is no third option, which makes them the single highest-value visual target you have.
- Key templates. The three or four page layouts that define the product's feel — the dashboard shell, the pricing page, the primary marketing hero. Not every page. The load-bearing ones.
- Individual components in isolation, snapshotted on a fixture page in each meaningful state: default, hover, disabled, error. A token rename that recolors half the app fails here first, on the component that owns the token.
Everything else stays behavior-tested. The checkout flow, the search results, the settings form — assert what they do, not how they look, because their content moves on its own and a pixel diff there is a false-positive factory.
The mechanics: baseline, mask, diff
In TestVibe this runs through the Visual plugin. Enable it, and generated tests can call a snapshot step against the live page:
await page.visual.matchSnapshot({ name: 'pricing-card', threshold: 0.01 });
The first run has no baseline, so it establishes one — it screenshots the live page, stores it as project content in a reserved visual-baselines/ folder in the Asset Library, and passes with a note that a baseline was created. Every later run screenshots the current render and pixel-diffs it against that stored baseline. Within the threshold, it passes. Beyond it, the scenario fails and attaches the expected, actual, and diff images to the run — you don't read a stack trace, you look at three pictures and see the drift immediately, per scenario, alongside every other artifact.
The reference pages will have regions that legitimately move — a "last updated" date, a live count, an embedded preview. Mask them so they don't trigger false failures:
await page.visual.ignoreRegion({ selector: '[data-testid="last-updated"]' });
await page.visual.matchSnapshot({ name: 'style-guide-buttons', threshold: 0.01 });
This is the discipline the plugin system is built for: visual steps live as ordinary Gherkin alongside functional ones, so a single run checks that the component works and that it still looks like itself — without the visual assertions leaking into the flows where they'd only add noise.
Accepting a new look is supposed to be deliberate
The objection to visual regression is always the same: "our design changes constantly, we'd be updating baselines forever." That's the point, not the problem.
When a diff fails, you have exactly two paths. Either it's unintended — drift — and you fix the code until the render matches the baseline again. Or the new look is intentional, and you accept it by deliberately deleting the baseline asset and re-running, which establishes the new render as the new truth. From the UI or the CLI, but never automatically. There's no "update all snapshots" button that quietly rubber-stamps whatever shipped.
That friction is a feature when you're defending a design system. An intentional visual change should require an intentional act — someone consciously saying "yes, the button looks different now, and that's correct." Auto-accepting diffs is how drift launders itself into the baseline: the suite goes green not because the UI is right but because it agreed to whatever it was shown. The delete-and-re-run step forces a human to look at the before and after and decide. That decision, made on purpose, is the entire mechanism by which a design system stays a source of truth instead of becoming a historical document.
The suite is the enforcement
A design system is a set of promises about how the product looks and behaves. Documentation states the promises. Tokens encode them. But nothing enforces them until something fails when they're broken — and functional tests, by design, never will. Pin your reference pages and key templates with visual baselines, mask the regions that move, keep behavior tests for everything else, and treat every intentional look change as a conscious baseline update. Do that, and drift stops being something you discover in a screenshot six weeks late. It becomes a failing scenario, with a diff image, on the run that introduced it.
Want AI-generated Playwright tests that check behavior and appearance together, verified against a live run of your app before you see them? Get early access, or read how plugins extend what a test can check.