Cypress Visual Regression Testing: A Practical Guide
Cypress is the most popular JavaScript end-to-end testing framework. Out of the box, it doesn't include visual regression — but adding it is straightforward, and you have several good options depending on your needs.
This guide covers three approaches: the lightweight cypress-image-snapshot plugin, Percy's cloud-based visual testing, and Applitools Eyes.
Why Visual Regression Matters in Cypress
Cypress tests verify behavior: clicks navigate correctly, forms submit, API calls return expected data. They don't catch visual regressions:
- A button that exists but is hidden behind another element
- Text that overflows its container after a font change
- A layout that breaks on specific viewport sizes
- Colors that change after a CSS refactor
Visual regression fills this gap. Instead of asserting that button.submit exists, you assert that the entire page looks correct compared to a known-good baseline.
Option 1: cypress-image-snapshot (Local, Zero Cost)
cypress-image-snapshot stores snapshots in your repository and diffs them on every run. It's free, works offline, and integrates natively with Cypress.
Installation
npm install --save-dev cypress-image-snapshotRegister the plugin in cypress/plugins/index.js (Cypress <10) or cypress.config.js (Cypress 10+):
// cypress.config.js (Cypress 10+)
const { defineConfig } = require('cypress')
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
addMatchImageSnapshotPlugin(on, config)
return config
}
}
})Add the command in cypress/support/commands.js:
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command'
addMatchImageSnapshotCommand()Writing Visual Tests
describe('Homepage', () => {
it('matches visual baseline', () => {
cy.visit('/')
cy.matchImageSnapshot('homepage')
})
it('hero section looks correct', () => {
cy.visit('/')
cy.get('[data-testid="hero"]').matchImageSnapshot('hero-section')
})
})First run creates the baseline in cypress/snapshots/. Subsequent runs compare against it.
Configuring Thresholds
Some pixel-level differences are acceptable (anti-aliasing, subpixel rendering). Configure tolerance:
cy.matchImageSnapshot('homepage', {
failureThreshold: 0.03, // 3% pixel difference allowed
failureThresholdType: 'percent',
customDiffConfig: { threshold: 0.1 }
})Updating Baselines
When a visual change is intentional, update the snapshots:
cypress run --env updateSnapshots=trueOr for interactive mode:
cypress open --env updateSnapshots=trueCI Considerations
Snapshots must be consistent across machines. Use a Docker image to ensure identical rendering:
# .github/workflows/visual-tests.yml
jobs:
visual-tests:
runs-on: ubuntu-latest
container:
image: cypress/browsers:latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npx cypress run
- uses: actions/upload-artifact@v3
if: failure()
with:
name: snapshot-diffs
path: cypress/snapshotsCommit the baseline snapshots to your repository. If CI snapshots differ from local ones, the fix is usually the Docker image — not the snapshots.
Option 2: Percy + Cypress
Percy is a cloud-based visual testing service that handles baseline management, browser-level rendering, and team review workflows. It's free up to 5,000 snapshots/month.
Installation
npm install --save-dev @percy/cli @percy/cypressImport Percy in cypress/support/commands.js:
import '@percy/cypress'Writing Percy Tests
describe('Pricing page', () => {
it('renders correctly', () => {
cy.visit('/pricing')
cy.percySnapshot('Pricing page')
})
it('mobile layout', () => {
cy.viewport(375, 812)
cy.visit('/pricing')
cy.percySnapshot('Pricing page - mobile')
})
})Running with Percy
npx percy exec -- cypress runPercy uploads screenshots to its dashboard where your team reviews and approves changes.
Percy in CI
- name: Run Cypress with Percy
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
run: npx percy exec -- cypress runPercy integrates with GitHub, GitLab, and Bitbucket to post PR status checks. Reviewers see a visual diff before merging.
Option 3: Applitools Eyes + Cypress
Applitools uses AI to filter rendering noise and flag only meaningful visual differences. It handles cross-browser testing through the Ultrafast Grid — running visual checks on 80+ browser/OS/device combinations from a single test run.
Installation
npm install --save-dev @applitools/eyes-cypress
npx eyes-setupWriting Eyes Tests
import '@applitools/eyes-cypress/commands'
describe('Dashboard', () => {
it('visual check', () => {
cy.visit('/dashboard')
cy.eyesOpen({
appName: 'My App',
testName: 'Dashboard visual check',
})
cy.eyesCheckWindow({
tag: 'Dashboard',
target: 'window',
fully: true
})
cy.eyesClose()
})
})Cross-Browser Configuration
// applitools.config.js
module.exports = {
testConcurrency: 5,
browser: [
{ width: 1280, height: 800, name: 'chrome' },
{ width: 1280, height: 800, name: 'firefox' },
{ width: 1280, height: 800, name: 'safari' },
{ deviceName: 'iPhone 12', screenOrientation: 'portrait' }
]
}A single Cypress run tests all configured browsers simultaneously.
Choosing the Right Approach
| Approach | Cost | Setup | Best For |
|---|---|---|---|
| cypress-image-snapshot | Free | 15 min | Small teams, offline, simple comparisons |
| Percy | Free tier + paid | 30 min | Teams needing review workflows, PR integration |
| Applitools | Paid | 45 min | Cross-browser coverage, AI noise filtering |
For most projects starting out, cypress-image-snapshot is the right choice. Move to Percy or Applitools when you need review workflows, cross-browser coverage, or better handling of dynamic content.
Organizing Visual Tests
Keep visual regression tests separate from functional tests:
cypress/
e2e/
functional/ # click, form, navigation tests
visual/ # snapshot tests
snapshots/ # committed baselines (for cypress-image-snapshot)Run functional and visual tests on separate schedules. Functional tests on every commit; visual regression on PR merge or nightly.
Common Pitfalls
Dynamic content: Timestamps, user avatars, ads, and animations cause false positives. Either mask these elements or use a tool like Applitools that handles them automatically.
cy.matchImageSnapshot('homepage', {
blackout: ['[data-testid="timestamp"]', '.advertisement']
})Font loading: Tests that run before fonts load produce inconsistent snapshots. Add a wait:
cy.document().its('fonts.ready').then(() => {
cy.matchImageSnapshot('homepage')
})Scroll position: Ensure the page is fully scrolled into position before snapping. cy.scrollTo('top') before snapshots.
Viewport size: Always set an explicit viewport. Different viewport sizes produce different snapshots and both might be valid baselines.
Next Steps
Once visual regression is running in CI:
- Add snapshots for every major page at both desktop and mobile viewports
- Add snapshots for key states: empty states, error states, loading states
- Configure Percy/Applitools PR integration so visual changes require explicit approval before merging
- Review and update baselines as part of your design review process — not as an afterthought