Cypress visual regression testing
Cypress screenshots but does not compare. Here are the maintained plugins that add image diffing in 2026, which fork of cypress-image-snapshot to use, and how to keep the results stable.
The first thing to know is the thing the documentation buries: Cypress does not do visual regression testing natively. cy.screenshot() captures an image and writes it to disk. It does not compare anything, store baselines, or fail on a difference. Every visual workflow in Cypress comes from a plugin.
That matters because the plugin landscape has churned, and a lot of published guidance points at packages that are no longer maintained.
The maintained options
| Plugin | Notes | Best for |
|---|---|---|
@simonsmith/cypress-image-snapshot | Rewrite of the original; needs Cypress 15.10+ | Teams migrating from the original plugin |
cypress-visual-regression | Actively maintained, straightforward baseline handling | New setups wanting minimal configuration |
@frsource/cypress-plugin-visual-regression-diff | Best diff viewer; supports e2e and component testing | Teams who review diffs inside the Cypress UI |
Setting it up
This example uses @frsource/cypress-plugin-visual-regression-diff, because its in-runner diff viewer removes the most friction day to day.
npm i -D @frsource/cypress-plugin-visual-regression-diff// cypress/support/e2e.ts
import "@frsource/cypress-plugin-visual-regression-diff";// cypress.config.ts
import { defineConfig } from "cypress";
import { initPlugin } from "@frsource/cypress-plugin-visual-regression-diff/plugins";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
initPlugin(on, config);
return config;
},
baseUrl: "http://localhost:3000",
},
// Pin the viewport so a baseline is reproducible.
viewportWidth: 1280,
viewportHeight: 800,
});// cypress/e2e/pricing.cy.ts
describe("pricing page", () => {
it("has not changed visually", () => {
cy.visit("/pricing");
cy.matchImage();
});
it("matches a single element", () => {
cy.visit("/pricing");
cy.get("[data-cy=plan-card]").matchImage({
maxDiffThreshold: 0.01, // fail above 1% differing pixels
});
});
});Making Cypress screenshots deterministic
Cypress has a specific advantage and a specific disadvantage here. The advantage is that it runs in the browser, so stubbing network responses is trivial. The disadvantage is that it has no equivalent of Playwright's retrying visual assertion, so you must reach a stable state yourself before capturing.
Stop animations
// cypress/support/commands.ts
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);
});
});Freeze the clock and stub the data
beforeEach(() => {
cy.clock(new Date("2026-01-15T12:00:00Z").getTime(), ["Date"]);
cy.intercept("GET", "/api/metrics", { fixture: "metrics.json" }).as("metrics");
cy.visit("/dashboard");
cy.wait("@metrics");
});Wait for content, never for time
// Not this — too slow when fast, too short when slow.
cy.wait(3000);
// This: assert the state that means the page is genuinely ready.
cy.get("[data-cy=skeleton]").should("not.exist");
cy.get("[data-cy=results-table]").should("be.visible");
cy.document().its("fonts.status").should("equal", "loaded");
cy.matchImage();The remaining causes, in the order worth fixing them, are in how to fix flaky visual tests.
The baseline problem
As with every self-hosted visual setup, images recorded on macOS will not match images rendered on a Linux CI runner — font hinting and antialiasing differ enough to fail on every capture. Record baselines inside the container CI uses:
docker run --rm --network host \
-v "$(pwd):/e2e" -w /e2e \
cypress/included:16.2.0 \
--spec "cypress/e2e/**/*.cy.ts" --env updateSnapshots=truePin the image tag to the Cypress version in your package.json. When they drift apart you get a wave of diffs that look like product regressions and are not.
Cypress or Playwright for visual work?
If you already run Cypress, the plugins above are a reasonable path and there is no need to migrate a working suite for this alone. If you are choosing fresh and visual testing is a significant motivation, Playwright has the stronger story: comparison is built in rather than delegated to a third-party plugin, and toHaveScreenshot() retries until the image is stable, which eliminates a whole category of the timing flake you otherwise handle by hand.
Comparison of both against the hosted options in 10 visual regression testing tools, compared, and the Playwright equivalent of this guide is here.
Frequently asked questions
Does Cypress support visual regression testing natively?
No. Cypress can capture screenshots with cy.screenshot(), but it cannot compare images, store baselines, or fail a test on a visual difference. Image comparison requires a plugin such as cypress-visual-regression, @frsource/cypress-plugin-visual-regression-diff, or @simonsmith/cypress-image-snapshot.
Which cypress-image-snapshot package should I use?
Use simonsmith/cypress-image-snapshot, a maintained rewrite of the original jaredpalmer package which is no longer actively developed. It requires Cypress 15.10 or later for Cypress.expose support and works with Cypress 15.x and 16.x.
Why do Cypress visual tests fail in CI but pass locally?
Font rendering and antialiasing differ between operating systems, so a baseline recorded on macOS will not match one rendered on a Linux CI runner. Record baselines inside the same cypress/included Docker image your CI uses, pinned to your Cypress version.