ScreenshotNeo

BlogGuides

Cypress Visual Testing: A Practical Guide

Capture stable Cypress screenshots, compare them with approved baselines, and review visual changes without confusing expected updates for regressions.

By the ScreenshotNeo team4 October 202613 min read

Cypress runs your application in a real browser and can capture screenshots, but cy.screenshot() alone does not detect visual changes. For visual regression testing, drive the page into a stable state, capture an image, compare it with an approved baseline using a plugin or image-diff tool, then review and accept only intentional changes. The comparison step is what turns screenshot capture into visual testing. Cypress’s visual testing guide describes this capture, compare, and review workflow.

This guide shows a minimal local workflow using Cypress, PNG screenshots, and a small Node.js image-diff script. It also covers Cypress screenshot settings, page and component coverage, stability, tool selection, troubleshooting, and when to use a screenshot API instead.

1. Understand what Cypress does—and what it does not do

Cypress supplies browser-based test execution and screenshot capture. Its built-in screenshot command does not compare an image to a baseline, report a visual diff, or manage approvals. Add a comparison layer for those tasks.

A useful visual test has four steps:

  1. Set up the state: visit a page, load deterministic data, and perform the actions that reveal the UI you want to check.
  2. Capture: save a viewport, full-page, or element screenshot.
  3. Compare: compare the current image with an approved baseline, optionally allowing a small number or percentage of changed pixels.
  4. Review: investigate the difference. Keep the old baseline if the change is a defect; update it if the design change is intentional.

A changed pixel is not automatically a bug. Baselines are review artifacts: the expected image should represent a deliberate, accepted design state. Cypress’s guide to visual testing explains that plugins and services add comparison and review workflows to Cypress captures.

2. Build a small local visual test

The example below captures a page after stubbing its API response, then compares the PNG against a baseline with pixelmatch. It is intentionally a small local workflow: your repository stores the baseline, and you inspect the generated diff image when a comparison fails.

Install the dependencies

npm install --save-dev cypress pixelmatch pngjs

Use a Cypress configuration that points to your local application. Adjust the base URL and spec pattern to match your project. This example uses the current Cypress configuration shape; see the Cypress configuration reference for project-specific settings.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.js',
    supportFile: 'cypress/support/e2e.js',
  },
  screenshotsFolder: 'cypress/screenshots',
})

Create the support file to set reliable screenshot defaults. Timers and CSS animations are disabled during screenshots by default; setting the intent explicitly makes the behavior visible to the team. Use blackout for sensitive or inherently variable regions, but remember that a blackout hides differences in those regions rather than proving they are correct.

// cypress/support/e2e.js
Cypress.Screenshot.defaults({
  capture: 'viewport',
  disableTimersAndAnimations: true,
  blackout: ['[data-visual-ignore]'],
})

Write a test that waits for the UI to reach the state being captured. Stub data that otherwise changes between runs. Replace /api/products and the selectors with routes and elements from your application.

// cypress/e2e/catalog.cy.js
it('renders the catalog consistently', () => {
  cy.intercept('/api/products', { fixture: 'products.json' }).as('products')
  cy.visit('/catalog')
  cy.wait('@products')
  cy.get('[data-testid="product-grid"]').should('be.visible')
  cy.get('[data-testid="product-card"]').should('have.length', 3)
  cy.screenshot('catalog', { capture: 'viewport' })
})

Put the fixture at cypress/fixtures/products.json, matching the application’s expected response structure. The test’s DOM assertions confirm the intended state exists; the screenshot comparison checks its rendered appearance. Neither check replaces the other.

Compare the captured PNG with a baseline

Cypress writes the screenshot under cypress/screenshots. Add this script as scripts/compare-visual.js. It compares the expected baseline and current capture, writes a diff image, and exits with a nonzero status when more than the configured fraction of pixels differ.

// scripts/compare-visual.js
const fs = require('node:fs')
const path = require('node:path')
const { PNG } = require('pngjs')
const pixelmatch = require('pixelmatch')

const name = process.argv[2]
if (!name) {
  console.error('Usage: node scripts/compare-visual.js <name>')
  process.exit(2)
}

const baselinePath = path.join('visual-baselines', `${name}.png`)
const actualPath = path.join('cypress/screenshots', 'catalog.cy.js', `${name}.png`)
const diffPath = path.join('visual-diffs', `${name}.png`)

for (const file of [baselinePath, actualPath]) {
  if (!fs.existsSync(file)) {
    console.error(`Missing image: ${file}`)
    process.exit(2)
  }
}

const baseline = PNG.sync.read(fs.readFileSync(baselinePath))
const actual = PNG.sync.read(fs.readFileSync(actualPath))
if (baseline.width !== actual.width || baseline.height !== actual.height) {
  console.error(`Image dimensions differ: baseline ${baseline.width}x${baseline.height}, actual ${actual.width}x${actual.height}`)
  process.exit(1)
}

const diff = new PNG({ width: baseline.width, height: baseline.height })
const changedPixels = pixelmatch(
  baseline.data,
  actual.data,
  diff.data,
  baseline.width,
  baseline.height,
  { threshold: 0.1 },
)

fs.mkdirSync(path.dirname(diffPath), { recursive: true })
fs.writeFileSync(diffPath, PNG.sync.write(diff))

const allowedFraction = Number(process.env.VISUAL_ALLOWED_FRACTION ?? 0)
const changedFraction = changedPixels / (baseline.width * baseline.height)
console.log(`${changedPixels} changed pixels (${(changedFraction * 100).toFixed(4)}%); diff: ${diffPath}`)

if (changedFraction > allowedFraction) {
  console.error('Visual comparison failed. Inspect the diff and update the baseline only for an intended change.')
  process.exit(1)
}

The generated screenshot folder includes the spec name, so adjust actualPath if your spec filename or folder differs. Add scripts for capture and comparison to package.json:

{
  "scripts": {
    "cy:visual": "cypress run --spec cypress/e2e/catalog.cy.js",
    "visual:compare": "node scripts/compare-visual.js catalog",
    "visual:check": "npm run cy:visual && npm run visual:compare"
  }
}

To create the first baseline, run the Cypress test once and copy the captured PNG to visual-baselines/catalog.png. Review the image before accepting it. Then run npm run visual:check on later changes. A failing comparison writes visual-diffs/catalog.png; inspect that image and the current capture before deciding whether to update the baseline.

The example uses a zero tolerance by default. Pixel-diff libraries can expose threshold and anti-aliasing controls; any nonzero allowance should be justified by the rendering noise you observe in your pinned environment. Avoid raising a page-wide threshold to make unstable tests pass: mask or stabilize a specific dynamic region instead.

3. Choose what to capture

Capture target Use it for Tradeoff
Viewport A visible state such as a dialog, navigation menu, or checkout step Does not cover content below the viewport
Full page Page-level layout, long forms, or article structure More content can create unrelated diffs; sticky elements and lazy content need care
Element A focused component or region with a clear owner May miss interactions with surrounding layout
Component test A component rendered in a controlled state and data environment Does not verify the full application flow around it

Cypress supports viewport, full-page, and runner capture modes in its Screenshot API. A runner capture includes the Cypress Test Runner and is useful for debugging; it is generally not the right baseline for the application’s appearance. For full-page capture, Cypress scrolls and stitches screenshots, which can affect fixed and sticky elements. Prefer an element or viewport snapshot when those effects are not part of what you need to verify.

Component testing can reduce the surface area of a visual test. Cypress mounts a component in a browser canvas, allowing the test to supply props or data for states such as error, loading, and empty. Use page-level end-to-end snapshots where the integration among components is itself important. Cypress also confirms that visual testing tools can check charts and graphs; use deterministic chart data and control animation or time-dependent axes when capturing them. See the Cypress FAQ and component styling guide.

4. Keep captures stable

Most noisy visual tests fail because the page is not in the same state on each run. Stabilize inputs to rendering before adjusting comparison thresholds.

  • API responses: use cy.intercept() fixtures for data that changes over time, as in the example above. Wait for the aliased request and assert the target content is present before capturing.
  • Fonts and images: wait for the relevant content to be visible and for layout to settle. Ensure the CI environment can load local fonts and required assets.
  • Clocks and dates: freeze time in the test where possible, or hide a changing timestamp with a targeted selector. Cypress documents onBeforeScreenshot and onAfterScreenshot callbacks for temporary DOM changes around a capture.
  • Animations: Cypress disables timers and CSS animations during capture by default. Keep that behavior for static snapshots. If the animation itself is the subject of the test, capture a deliberate frame and control its timing.
  • Random content and IDs: seed randomness or provide stable fixtures. Avoid tests whose visible content depends on the current time, random values, or external services.
  • Consent banners and popups: decide whether the UI state is part of the requirement. Dismiss the banner to test the page behind it, or capture it in a separate test when consent UI matters.
  • Personal or sensitive information: use synthetic test data. Cypress’s blackout option can conceal selected elements in captures, but masking is not a substitute for safe test data.

For example, Cypress lets you hide a clock before a screenshot and restore it afterward:

// cypress/support/e2e.js
Cypress.Screenshot.defaults({
  onBeforeScreenshot($el) {
    $el.find('.clock').hide()
  },
  onAfterScreenshot($el) {
    $el.find('.clock').show()
  },
})

These callbacks apply to non-failure screenshots. Keep the selector narrow: hiding a large region may conceal the regression the test was meant to detect. See the Screenshot API options and callbacks for capture configuration details.

5. Configure Cypress screenshots

Set defaults once in the support file, or pass options to an individual cy.screenshot() call when a particular capture needs different behavior. Cypress documents these relevant defaults:

Option What it controls Visual testing note
capture viewport, fullPage, or runner Choose the application area that matches the assertion. Failure screenshots are coerced to runner capture.
scale Whether to scale the app to fit the viewport Runner captures force scaling; keep viewport and browser configuration consistent.
disableTimersAndAnimations Whether JavaScript timers and CSS animations are disabled during capture Defaults to true; consider what the component needs to render before capture.
blackout Selectors for elements to black out Useful for a narrow dynamic or sensitive region; applies to viewport captures, not runner captures.
screenshotOnRunFailure Automatic screenshots after failures in cypress run Defaults to true; separate debugging artifacts from baseline snapshots.
overwrite Whether duplicate screenshot names overwrite each other Defaults to false; use clear unique names to avoid confusing artifacts.
onBeforeScreenshot, onAfterScreenshot Synchronous callbacks before and after a non-failure capture Can temporarily adjust the DOM to make a capture repeatable.

For exact behavior and per-command options, use the Cypress Screenshot API reference and cy.screenshot() command reference. In particular, failure screenshots are useful debugging evidence, but should not automatically become visual baselines: a failed test may capture a different state from the checkpoint your visual test expects.

6. Select a comparison and review workflow

The right setup depends on where you want image files stored, how changes are reviewed, and whether tests need multiple browsers or viewport sizes. Cypress’s visual testing overview describes local open-source plugins and hosted visual services. Its plugin directory marks community-owned extensions as not reviewed by Cypress, so check each project’s maintenance and Cypress compatibility before adding it.

Approach Fits when Plan for
Local image-diff plugin or script You want comparisons in your own repository and CI Baseline files, diff artifacts, update rules, and a consistent rendering environment
Hosted visual review service Your team needs a dashboard, approvals, history, pull request review, or hosted cross-browser rendering Service setup, snapshot upload, team review process, and subscription terms
Component visual coverage You want focused checks for shared components and controlled states Component mounting setup plus separate end-to-end coverage for key user flows

Options named in Cypress materials include Percy, Applitools, Chromatic, and community image-diff plugins. Their workflows and features differ, and the research for this guide does not establish neutral current pricing or a performance ranking. Check the products’ current official setup documentation and the Cypress plugin directory for compatibility before choosing. Evaluate a candidate by baseline storage, review and approval flow, supported test types, browser coverage, and how the team will maintain it.

7. Troubleshoot common failures

Symptom Likely cause Fix
Every run produces a diff Uncontrolled API data, clock, random content, font loading, or browser differences Stub responses, freeze or hide changing values, wait for the relevant state, and keep the CI browser and viewport consistent.
The screenshot is blank or incomplete The capture ran before the page or target content finished rendering Wait for the request and a visible, meaningful element; assert the expected content before capturing.
Screenshot looks different from the interactive app Timers or animations are paused for the capture, or the app changed during the asynchronous screenshot operation Check screenshot defaults and capture timing. Cypress notes screenshots are asynchronous; wait for a stable state and avoid relying on transient frames.
Comparison reports different dimensions Viewport, device scale, full-page layout, or browser rendering changed Pin viewport and browser configuration, then regenerate the baseline only if the new dimensions are intended.
Full-page screenshot has odd sticky elements Full-page capture scrolls and stitches multiple images Use viewport or element capture if sticky behavior is irrelevant, or verify the full-page result against the Cypress screenshot documentation.
Diff is too noisy to review The screenshot covers too much unrelated UI or includes dynamic third-party content Capture a smaller element or mask a specific variable region. Avoid broad thresholds that can hide real changes.
Plugin command is undefined The package was installed but its support-file registration or setup step was missed Follow that plugin’s official install instructions and confirm the support file is loaded. Cypress notes that installing a plugin alone may not register it.
Baseline update hides a regression A new image was accepted without reviewing the rendered change Inspect the current, expected, and diff images before updating; require a reviewer for intentional UI changes.

See Cypress’s screenshot command notes for asynchronous capture behavior and its plugin setup guide for plugin registration and where plugin code runs.

8. Performance, reliability, and cost

Performance

Each screenshot adds capture and image-comparison work. Keep snapshots focused on meaningful states rather than taking one after every action. Element-level snapshots reduce unrelated changes and make diffs quicker to inspect; reserve full-page captures for checks that need page-wide coverage. Hosted tools may do rendering or comparison work in their own infrastructure, while local comparisons consume CI resources; the actual cost and run time depend on the selected tool and suite.

Reliability

Visual checks are most reliable when the browser, viewport, fonts, test data, and application state are stable. Keep a baseline’s provenance clear, review updates, and make diff images available as CI artifacts. A stable visual test can still miss behavior that is not visible in the captured state, and an image diff cannot establish accessibility, interaction correctness, or business logic. Pair visual checks with functional and accessibility tests.

Cost

Cypress screenshot capture and local image comparison do not imply a hosted visual-testing subscription; plugin and service costs depend on the chosen option. This research does not establish current prices across services, so check vendors’ official pricing pages before budgeting. Include engineer review time and baseline maintenance in the cost of a visual-testing workflow.

Or skip the browser setup

If your goal is a clean screenshot of a public page rather than a browser-driven regression test, ScreenshotNeo is a website screenshot API and MCP server. It does not replace Cypress assertions or your application’s baseline workflow; it provides a one-call way to capture a URL. The ScreenshotNeo documentation covers the API.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers say which page verdict and billing status applied. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Can Cypress compare screenshots without a plugin?

No. Cypress captures screenshots; use a plugin, an image-diff script, or a visual-testing service to compare them with baselines.

Can Cypress visual testing cover charts and graphs?

Yes. Capture a chart in a deterministic state with stable data and controlled animation. Cypress’s FAQ specifically addresses visual testing for charts and graphs.

Should I snapshot every test?

No. Snapshot representative pages, shared components, and important user states. Every baseline change requires review, so keep the set intentional.

Does a passing visual test mean the page is accessible?

No. Image comparison checks rendered appearance against an image; use accessibility checks for requirements such as contrast and semantics.

When should I choose component tests over end-to-end screenshots?

Use component tests for isolated states with controlled inputs. Use end-to-end snapshots when the complete page or user flow is what needs visual verification.