Q40 of 48 · Cypress

How do you handle visual regression in Cypress without a paid service?

CypressSeniorcypressvisual-regressionsnapshotssenior

Short answer

Short answer: Use `cypress-image-snapshot` or `cypress-visual-regression` to capture and diff PNGs in CI. Store baselines in git or an S3 bucket, fail on diffs above a tolerance threshold, and let developers update baselines via a flag (`--env updateSnapshots=true`) when changes are intentional.

Detail

Paid services like Percy and Applitools are convenient but cost real money for large suites. Free alternatives that work well for most teams:

cypress-image-snapshot (or its modern fork @simonsmith/cypress-image-snapshot) captures a screenshot of an element or full page and compares against a stored baseline. The first run writes the baseline; subsequent runs diff against it.

cy.get('[data-test=dashboard-card]').matchImageSnapshot('dashboard-card', {
  failureThreshold: 0.01,        // 1% of pixels can differ
  failureThresholdType: 'percent',
});

Storage — baselines go into the repo (git LFS for many large images) or an S3 bucket keyed by spec name. Repo-storage keeps things simple and reviewable as part of PRs; S3 scales better.

The CI workflow:

  1. PR runs Cypress; visual diffs above threshold fail.
  2. Developer reviews the diff (pixelmatch or odiff produces a side-by-side image).
  3. If the change is intentional, re-run with --env updateSnapshots=true to regenerate baselines.
  4. The new baseline is committed in the same PR.

Practical guard rails:

  • Use stable inputs. Real-time data, randomised IDs, current dates — all break visual diffs. Stub them.
  • Hide volatile elements. A "loading…" spinner mid-snapshot causes false positives. cy.get('[data-test=spinner]').should('not.exist') first.
  • Set a tolerance. Exact-pixel matching is too strict for cross-browser. 0.5–2% is realistic.
  • Limit to critical components. Don't snapshot every page — just visually-important ones (homepage, product card, checkout button states).

Cross-browser visual is harder without a paid service because Cypress only ships Chrome/Firefox/Edge support reliably. Most teams pick one browser as the visual baseline.

The trade-off vs paid services: paid tools have better diffing UI, integration with PR review, and cross-browser handling. Free works well if you're disciplined about snapshot hygiene; it gets painful at scale (200+ snapshots).

// EXAMPLE

visual.cy.ts

import { addMatchImageSnapshotCommand } from '@simonsmith/cypress-image-snapshot/command';
addMatchImageSnapshotCommand({ failureThreshold: 0.01, failureThresholdType: 'percent' });

it('checkout summary renders correctly', () => {
  cy.intercept('GET', '/api/cart', { fixture: 'cart-fixed.json' });
  cy.clock(new Date('2026-01-01').getTime());      // freeze time-relative UI
  cy.visit('/checkout');
  cy.get('[data-test=spinner]').should('not.exist');
  cy.get('[data-test=summary]').matchImageSnapshot('checkout-summary');
});

// To intentionally update baselines:
//   npx cypress run --env updateSnapshots=true --spec cypress/e2e/visual.cy.ts

// WHAT INTERVIEWERS LOOK FOR

Naming a free plugin (`cypress-image-snapshot`), the baseline-update workflow, and the discipline around stable inputs / tolerances.

// COMMON PITFALL

Snapshotting volatile UI without stubbing time, data, and animations — every PR then has unrelated visual diffs and the team stops looking.