Q40 of 48 · Cypress
How do you handle visual regression in Cypress without a paid service?
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:
- PR runs Cypress; visual diffs above threshold fail.
- Developer reviews the diff (pixelmatch or odiff produces a side-by-side image).
- If the change is intentional, re-run with
--env updateSnapshots=trueto regenerate baselines. - 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