Updating visual baselines without approving regressions
This page is part of the Baseline Management section of the Visual Regression & Snapshot Strategies guide. It addresses the single most common way visual regression testing fails silently: a developer sees a wall of red diffs, runs --update-snapshots to make them green, and in doing so promotes real regressions to the baseline where the tooling can never catch them again.
Problem statement
Visual regression tools are only as trustworthy as the baseline they compare against. The moment you accept a bad baseline, every subsequent run compares new output against a wrong reference and reports success. The regression is now invisible.
The failure mode is procedural, not technical. A branch changes a shared spacing token. The visual test run comes back with 60 changed screenshots — 4 the developer meant to change, 56 unintended knock-on shifts across unrelated components. Facing 60 red diffs and a deadline, the developer runs the one command that makes it all green:
npx playwright test --update-snapshots
Every baseline is overwritten. The 4 intended changes and the 56 regressions are now equally “correct.” The pull request shows a large but plausible-looking diff, a reviewer skims it, and a spacing regression across half the component library ships. The next run is green. Nobody will ever be alerted to it again.
Root cause
The root cause is treating baseline update as a build step instead of a review decision. --update-snapshots (and its equivalents) is a blunt overwrite: it records whatever rendered this run as the new truth, with zero judgment about whether each change was intended. When a change has wide blast radius — a design token, a global stylesheet, a font load, a tolerance-threshold adjustment — one blind update accepts dozens of unrelated shifts at once.
Correct baseline management inverts the default: a diff is guilty until a human reviews it. The baseline management discipline is to separate intended changes from regressions by eye, update only the intended stories, and make every baseline change a reviewable, owned event in the pull request — never a side effect of a command run to clear red.
Minimal reproduction
A shared token change produces both an intended diff and collateral regressions in one run:
/* tokens.css — the intended change is on Button; Badge shares the token */
:root {
--space-inline: 16px; /* PR intends: Button padding 12px → 16px */
}
$ npx playwright test
56 failed
✘ Badge › default — 41px shifted, UNINTENDED
✘ Chip › selected — 33px shifted, UNINTENDED
✘ Toolbar › compact — 88px shifted, UNINTENDED
...
4 failed
✘ Button › primary — 12px shifted, INTENDED
✘ Button › secondary — 12px shifted, INTENDED
The dangerous next move is --update-snapshots, which accepts all 60. The correct move is to review, then update only the four Button baselines.
Step-by-step fix
1. Review every diff before accepting anything
Generate the diff report and look at each changed screenshot. Never pipe a red run straight into an update.
# produce the HTML report with side-by-side expected/actual/diff images
npx playwright test --reporter=html
npx playwright show-report
What this does: the report renders every failing screenshot as expected-vs-actual-vs-diff so you can visually separate the four intended Button changes from the 56 collateral shifts. This is the judgment step the blind flag skips.
2. Update only the intended stories
Scope the update to the exact spec you changed. Do not update the suite.
# regenerate baselines ONLY for the Button spec you intended to change
npx playwright test button.spec.ts --update-snapshots
# Jest image snapshots: scope by test name instead
npx jest --ci=false -t "Button primary" -u
# Chromatic: accept individual stories in the review UI, not the whole build
# (click "Accept" per story, or: npx chromatic --auto-accept-changes=false)
What this does: only the four Button baselines are rewritten. The 56 regressions remain red, which is correct — they are bugs to fix in the token change or its consumers, not new truth to accept.
3. Commit baselines as a reviewable diff
Commit the regenerated PNGs in their own logical change so the image diff appears in the pull request.
git add tests/__screenshots__/button.spec.ts/
git commit -m "test: update Button baselines for 16px inline padding"
git status --short
# M tests/__screenshots__/button.spec.ts/button-primary-chromium.png
# M tests/__screenshots__/button.spec.ts/button-secondary-chromium.png
What this does: because only the Button baselines moved, the pull request’s file list is a precise record of which baselines changed. A reviewer opening the PR sees two changed images, not sixty — making an accidental regression obvious rather than buried.
4. Gate the snapshot directory with CODEOWNERS
Require a design-system owner to approve any change to a baseline image.
# .github/CODEOWNERS
# any baseline image change needs design-system review
/tests/__screenshots__/ @acme/design-system
/.storybook/ @acme/design-system
# .github/workflows/visual.yml — make the visual job a required check
name: visual-regression
on: pull_request
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test # fails on any un-updated baseline diff
What this does: CODEOWNERS forces a review from the design-system team on every PR that touches a baseline, and the visual job — configured as a required status check in branch protection — blocks merge until both the diffs are green and an owner has approved. A regression can no longer be committed as a quiet side effect of unrelated work.
Verification
Confirm that only the intended baselines moved and that the collateral regressions are still caught.
# 1. exactly the intended baselines changed — nothing else
git diff --stat main -- tests/__screenshots__/
# tests/__screenshots__/button.spec.ts/button-primary-chromium.png | Bin
# tests/__screenshots__/button.spec.ts/button-secondary-chromium.png | Bin
# 2 files changed
# 2. re-run: Button green (accepted), Badge/Chip still red (unfixed regressions)
npx playwright test
# ✓ Button › primary
# ✓ Button › secondary
# ✘ Badge › default — 41px shifted ← still flagged, as it should be
# 56 failed
The signal of success is asymmetry: the intended Button baselines pass, and the 56 collateral regressions are still red. A blind --update-snapshots would have shown all 60 green — which is exactly the false confidence to avoid. Fix the token blast radius (or the affected components), then repeat the review-and-scope loop until every remaining diff is deliberate.
Edge cases and caveats
- Intended global changes still need per-story confirmation. Sometimes a token change is meant to move 56 components. Even then, do not blind-update: page through the diff report to confirm all 56 moved the way you expected, because a global change often produces one or two components that break rather than adapt. Scope the update after you have eyeballed the set, not before.
- Baseline PNGs are environment-sensitive. Regenerating baselines on your laptop then running them against Linux CI will diff on font hinting and anti-aliasing. Always regenerate baselines inside the same container CI uses — see handling font rendering differences across browsers — or CI will re-flag baselines you just “fixed.”
- Chromatic auto-accept undermines the whole discipline. Setting
--auto-accept-changeson a branch is the hosted-tool equivalent of blind--update-snapshots. Reserve it for known-throwaway branches; on any branch that merges tomain, require explicit per-story acceptance in the review UI.
FAQ
Why is running --update-snapshots blindly dangerous?
The flag overwrites every baseline with whatever the current run produced, with no distinction between an intended change and a regression. If a shared token, a global stylesheet, or a font swap altered dozens of unrelated components, a blind update accepts all of those changes as the new correct state. The regressions are now the baseline, so future runs pass and the tool can never flag them again.
How do I update only the baselines I intended to change?
Scope the update command to the specific test file or story instead of the whole suite. In Playwright, pass the spec path with --update-snapshots; with Jest image snapshots, pass a -t name filter or the test path; in Chromatic, accept individual stories in the review UI rather than accepting the entire build. Scoping means only the components you deliberately edited get new baselines.
How does CODEOWNERS help protect visual baselines?
Adding the snapshot directory to CODEOWNERS makes a review from a designated owner mandatory before any pull request that touches a baseline image can merge. Combined with a required status check on the visual test job, it guarantees that a human who owns the design system looks at every baseline change, so a regression cannot be quietly committed alongside an unrelated code change.
Related
- Baseline Management — the parent section covering how to store, version, and evolve visual baselines safely.
- Configuring Chromatic Threshold Settings for Pixel-Perfect Diffs — tune diff sensitivity so real changes surface without drowning review in noise.
- Handling Font Rendering Differences Across Browsers — keep regenerated baselines stable between local machines and CI.
- Making Visual Review a Required Status Check — enforce owner approval on baseline changes before merge.
- Percy vs Chromatic for Component Visual Testing — how each hosted service models baseline branching and per-story acceptance.