All posts
CypressVisual testingCI/CD

Cypress visual regression testing: setup and the plugin trap

A
Ananya Rao
QA Lead
June 6, 2026
12 min read

Plenty of teams are committed to Cypress for end-to-end testing and want to add visual regression without switching frameworks. It's absolutely doable — but unlike Playwright, which ships toHaveScreenshot() in the box, Cypress has no built-in screenshot comparison. You capture images natively; you bring your own diffing.

This guide covers both realistic routes: wiring up a community plugin yourself, and handing the comparison to a managed service. More importantly, it covers the part nobody warns you about — making captures deterministic, which is where most Cypress visual suites actually fail.

Why Cypress has no native visual diffing

Cypress gives you cy.screenshot(), which writes a PNG to disk. That's it. Comparing that PNG against a known-good baseline — deciding what counts as 'different', storing baselines, reviewing changes — is deliberately left to the ecosystem. So every Cypress visual setup is really three decisions: how you capture, how you compare, and where baselines live.

Route 1: the plugin approach

The most common community option is cypress-visual-regression. Install it alongside Cypress:

npm install --save-dev cypress-visual-regression

Register it in your Cypress config so the diffing tasks run in Node:

// cypress.config.js
const { defineConfig } = require('cypress');
const { configureVisualRegression } = require('cypress-visual-regression');

module.exports = defineConfig({
  e2e: {
    env: { visualRegressionType: 'regression' },
    screenshotsFolder: './cypress/snapshots/actual',
    setupNodeEvents(on, config) {
      configureVisualRegression(on);
      return config;
    },
  },
});

Add the command to your support file, then call it from a test:

// cypress/support/e2e.js
import 'cypress-visual-regression/dist/support';

// cypress/e2e/home.cy.js
describe('home page', () => {
  it('matches the visual baseline', () => {
    cy.visit('/');
    cy.compareSnapshot('home');
  });
});

Run once with visualRegressionType set to 'base' to record baselines, then switch to 'regression' to compare against them. That's the happy path — and it works fine until your first CI run on a different machine.

The part that actually breaks: non-deterministic captures

A visual test only has value if an identical page produces an identical image. In practice a dozen things quietly change between runs. Fix these before you blame the diff engine.

Lock the viewport

Different viewport sizes produce different layouts and therefore guaranteed diffs. Pin it explicitly rather than relying on config defaults:

beforeEach(() => {
  cy.viewport(1280, 720);
});

Kill animations and transitions

A capture taken mid-transition is a coin flip. Inject a stylesheet that freezes motion before you screenshot:

Cypress.Commands.add('freezeUI', () => {
  cy.document().then((doc) => {
    const style = doc.createElement('style');
    style.innerHTML = `
      *, *::before, *::after {
        animation: none !important;
        transition: none !important;
        caret-color: transparent !important;
      }
    `;
    doc.head.appendChild(style);
  });
});

Wait for fonts, not just the network

Web fonts often load after your assertions pass, so the first run captures a fallback font and the next captures the real one. Every glyph shifts and the whole page reads as changed:

cy.document().its('fonts.status').should('equal', 'loaded');

Neutralise genuinely dynamic content

Timestamps, random avatars, 'posted 3 minutes ago', live counters and carousels will fail forever if you leave them in frame. Either stub the data, freeze the clock, or mask the region:

// Freeze time so relative dates are stable
cy.clock(new Date('2026-01-01T00:00:00Z').getTime());

// Or hide a volatile widget before capturing
cy.get('[data-test="live-feed"]').invoke('css', 'visibility', 'hidden');
  • Run captures in a consistent browser and OS — font rendering differs between macOS and Linux, so a baseline recorded on a laptop will not match CI.
  • Disable or account for scrollbars; they appear and disappear depending on content height and OS settings.
  • Prefer element captures over full-page ones when only a component matters — smaller surface, fewer false positives.
  • Seed test data so lists have the same number of rows every run.

Choosing a sensible threshold

Anti-aliasing and sub-pixel rendering mean two 'identical' screenshots are rarely byte-identical. A zero-tolerance threshold produces noise; an overly generous one hides real bugs. Start around 0.1–0.2% of differing pixels and tune from there, and prefer a comparison that understands perceptual difference rather than raw pixel equality. If your suite is flagging fifty diffs and forty-nine are noise, the threshold or the capture determinism is wrong — not the page.

The maintenance tax

The plugin route is legitimate and plenty of teams ship it. Just budget for the ongoing costs honestly:

  • Baseline storage — committing PNGs to git bloats the repo and makes review painful; the alternative is building your own artifact storage.
  • Cross-environment drift — baselines recorded locally rarely match CI, so someone ends up running everything in Docker to keep rendering identical.
  • Review workflow — there's no shared dashboard, so approving an intentional change means reading a diff image in a CI artifact.
  • Upkeep — the plugin has to stay compatible as Cypress majors land, and that job belongs to someone on your team.

Route 2: hand the comparison to a service

The alternative is to keep writing ordinary Cypress tests and let a managed service own baselines, comparison and review. With PixellPeep, install the SDK:

npm install --save-dev @pixellpeep/cypress @pixellpeep/client

Register the Node tasks in your Cypress config:

// cypress.config.js
import { defineConfig } from 'cypress';
import { registerPixellPeepTasks } from '@pixellpeep/cypress';

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      registerPixellPeepTasks(on, config, {
        apiUrl: process.env.PIXELLPEEP_API,
        apiKey: process.env.PIXELLPEEP_API_KEY,
        projectId: process.env.PIXELLPEEP_PROJECT_ID,
      });
      return config;
    },
  },
});

Import the command once in your support file:

// cypress/support/e2e.js
import '@pixellpeep/cypress/support';

Then snapshot from any test. The command captures via cy.screenshot() and fails the test when the comparison comes back outside tolerance:

describe('login', () => {
  it('looks right', () => {
    cy.viewport(1280, 720);
    cy.visit('/login');
    cy.pixellpeepSnapshot('login-page');
  });
});

Baselines live server-side, so there are no PNGs in your repo and no drift between a laptop and CI. You can pick a comparison algorithm per snapshot when a particular screen needs stricter or looser treatment, and intentional changes get approved in a shared dashboard instead of by committing a new image.

Running it in CI

Whichever route you choose, visual tests earn their keep in CI — catching the regression before review, not after release. A minimal GitHub Actions job:

name: e2e
on: [pull_request]

jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx cypress run
        env:
          PIXELLPEEP_API: ${{ secrets.PIXELLPEEP_API }}
          PIXELLPEEP_API_KEY: ${{ secrets.PIXELLPEEP_API_KEY }}
          PIXELLPEEP_PROJECT_ID: ${{ secrets.PIXELLPEEP_PROJECT_ID }}

Pin the runner image and the browser version. Upgrading either can shift font rendering and invalidate baselines, which looks exactly like a regression until you work out what changed.

Troubleshooting the usual failures

  • Everything differs by a tiny percentage — anti-aliasing. Raise the threshold slightly or use a perceptual comparison instead of exact pixel matching.
  • Text shifts by a pixel or two — a font loaded late, or the baseline came from a different OS. Wait on fonts.status and record baselines in the same environment CI uses.
  • Diffs only in CI, never locally — almost always rendering environment. Run locally in the same container image.
  • A thin band differs at the page edge — a scrollbar appeared. Fix the content height or hide the scrollbar during capture.
  • One region always differs — dynamic content. Stub it, freeze the clock, or mask the element.
  • Every snapshot fails after an upgrade — Cypress, the browser, or the base image changed. Re-record baselines deliberately rather than raising thresholds.

Frequently asked questions

Does Cypress support visual regression testing natively?

No. Cypress captures screenshots with cy.screenshot() but has no built-in image comparison. You add diffing through a community plugin such as cypress-visual-regression, or through a managed service.

Should I commit baseline images to git?

It works for a handful of snapshots and becomes painful quickly — binary files bloat the repository, diffs are unreviewable in a pull request, and baselines recorded on one machine rarely match CI. Shared or hosted storage scales better.

Why do my Cypress screenshots differ between local and CI?

Font rendering and anti-aliasing differ across operating systems. A baseline captured on macOS will not match one produced on a Linux CI runner. Record and compare in the same environment — usually a pinned Docker image — or use a service that normalises this for you.

Can I use Cypress visual testing without a paid tool?

Yes. cypress-visual-regression is open source and free. You trade money for maintenance: you own baseline storage, the review workflow, and keeping the toolchain compatible.

Where to land

If you have a small suite and someone willing to own the plumbing, the plugin route is perfectly reasonable — start there. If you're adding visual checks across a real application and want baselines, review and CI reporting to work without a maintenance owner, a managed integration removes the recurring cost. Either way, spend your first day on deterministic captures. Threshold tuning cannot rescue a screenshot that was never reproducible.

With Cypress, the question isn't 'can I do visual testing' — it's 'who maintains it'. Answer that before you write the first snapshot.