How to Compare Website Screenshots Using Cypress in a React Project
Cypress captures React pages, but visual comparison requires a plugin or hosted service. Build stable screenshot checks with repeatable data, fixed rendering, and reviewed baselines.
Cypress can render a React page or component and save a screenshot. Its built-in cy.screenshot() command does not compare that image with a baseline. To detect visual changes, add a Cypress-compatible image-diff plugin or use a hosted visual-testing service. The workflow is: render a known UI state, capture it, compare it with an approved baseline, review the diff, and update the baseline only when the change is intentional.
This guide uses Cypress component testing for a focused React example. The same principles apply to end-to-end tests that visit a running app. Cypress supports React projects using Vite, Webpack, and Next.js. See the Cypress React component testing guide and its visual testing guide.
1. Choose a screenshot comparison workflow
Cypress provides screenshot capture and test orchestration. A separate integration supplies the visual comparison assertion and baseline management.
| Workflow | Where comparison runs | Good fit when | Tradeoffs |
|---|---|---|---|
| Local or open-source plugin | Developer machine or CI | You want to keep image files and review within your project infrastructure, and can standardize the rendering environment. | Your team owns baseline storage, diff review, CI setup, and rendering consistency. |
| Hosted visual-testing service | Provider-managed service | You want managed baselines, dashboards or pull-request review, or broader browser and viewport coverage. | Check subscription pricing, data-handling terms, browser support, and the provider’s current workflow. |
Choose based on cost, where baselines live, who controls browser rendering, how reviewers approve changes, and how many browsers and viewport sizes you need. Cypress’s visual-testing guide lists tools such as Cypress Image Diff, Cypress Image Snapshot, Cypress Visual Regression, and Visual Regression Diff, as well as hosted options. The directory is an index, not an endorsement. Confirm current maintenance and compatibility with your Cypress version before installing.
For example, Applitools describes its Cypress integration as using Visual AI and hosted baselines, and says it renders across Chrome, Firefox, Safari, and Edge. Those are vendor-described capabilities; verify current product details directly with the provider. See the Applitools Cypress integration page.
2. Install and configure a visual-diff integration
Select one maintained integration compatible with your project’s Cypress version. The exact package name, setup command, configuration, and assertion API differ, so use that integration’s current installation documentation rather than copying an obsolete plugin setup. The example below uses a placeholder command deliberately: replace it with the install and configuration steps from the chosen plugin’s documentation.
# From the React project root, follow your selected plugin's current docs:
npm install --save-dev <visual-diff-package>
Configure the plugin in the Cypress support file or configuration file as its documentation specifies. A typical integration adds a custom command or assertion that captures an image, finds or creates a baseline, compares pixels or visual features, and saves a diff artifact when the images differ.
Before adopting it, confirm:
- Supported Cypress and Node.js versions.
- Whether first runs create baselines automatically or require an explicit approval/update mode.
- Where baseline and diff files are written, and whether CI uploads or retains them.
- How to set comparison thresholds, masks, ignored regions, and per-test options.
- How to inspect and approve changes without silently rewriting snapshots during ordinary test runs.
3. Make the React state repeatable
A screenshot is useful only if each run reaches the same intended state. Pin the viewport, supply stable data, wait for meaningful content, and settle animations before taking the checkpoint. Avoid time-based sleeps when an assertion can wait for the actual state.
Example component, src/PriceCard.jsx:
export function PriceCard({ name, amount, period }) {
return (
<article data-testid="price-card">
<h2>{name}</h2>
<p><strong>${amount}</strong> / {period}</p>
<button type="button">Choose plan</button>
</article>
);
}
Mount it in a Cypress component test. The visualMatch command below is illustrative: replace it with the actual command exported by your selected visual-diff integration.
import { mount } from 'cypress/react'
import { PriceCard } from '../../src/PriceCard'
describe('PriceCard visual appearance', () => {
it('matches the approved desktop baseline', () => {
cy.viewport(1280, 800)
mount(<PriceCard name="Starter" amount={12} period="month" />)
cy.findByTestId('price-card').should('be.visible')
cy.findByRole('heading', { name: 'Starter' }).should('be.visible')
// Replace with the screenshot comparison command from your plugin.
cy.findByTestId('price-card').visualMatch('price-card-starter-desktop')
})
})
If your setup does not include Testing Library commands such as findByTestId and findByRole, use Cypress selectors such as cy.get('[data-testid="price-card"]') and cy.contains('h2', 'Starter'). Keep selectors stable and aimed at the UI contract.
To compare a page through end-to-end testing, use the same state-control principles:
describe('pricing page visual appearance', () => {
it('matches the approved desktop baseline', () => {
cy.viewport(1280, 800)
cy.intercept('GET', '/api/plans', { fixture: 'plans.json' }).as('plans')
cy.visit('/pricing')
cy.wait('@plans')
cy.contains('h1', 'Plans').should('be.visible')
// Replace with your integration's comparison command.
cy.get('[data-testid="pricing-content"]').visualMatch('pricing-desktop')
})
})
The fixture keeps API data consistent. If the app loads fonts, images, or other required assets asynchronously, wait for an observable ready condition before capturing. Cypress screenshots are asynchronous; the API documentation describes capture as taking around 100ms and cautions that the application may change before the image is captured. Do not trigger state changes immediately before a capture and assume they have already rendered.
4. Capture the right area
Prefer the smallest representative component or page region that answers the test’s question. A narrow component checkpoint reduces unrelated changes and makes diffs easier to review. Use page screenshots when page composition itself matters.
Cypress supports element screenshots as well as viewport, full-page, and runner screenshots. Its screenshot command captures and saves an image; it does not perform the comparison. Consult the Cypress screenshot API for the current options and behavior.
- Element: focus on a card, dialog, navigation bar, or other component. Ensure the element is visible and not covered.
- Viewport: capture what a user sees without scrolling. This is useful for a controlled initial view.
- Full page: capture long content. Cypress scrolls and stitches the page, so fixed or sticky elements can repeat in the resulting image.
- Runner: capture the Cypress runner as well as the application when diagnosing the test itself; generally not the right baseline for product UI.
Choose one capture scope consistently for a given baseline. A full-page baseline and a viewport baseline answer different questions and should not share the same expected image.
5. Review diffs and update baselines safely
- Run the visual test in the agreed browser and environment.
- When it fails, inspect the actual image, baseline, and generated diff artifact together.
- Decide whether the change is a defect, environmental noise, or an intended design change.
- Fix defects or stabilize the test. For intentional UI changes, update the baseline using the integration’s explicit approval or update process.
- Review baseline changes in version control or the hosted review workflow, just like application changes.
Do not make baseline updates an automatic side effect of every CI run. That can turn a regression into the new expectation without review. Preserve diff artifacts long enough for a developer to investigate failures.
6. Reduce false positives
- Fix the viewport: set the same width and height for every run and name baselines by viewport when testing multiple sizes.
- Pin rendering: use a consistent browser version, operating system, display scaling, and installed fonts where possible. Pixel comparisons are sensitive to these differences.
- Control data: use fixtures or deterministic test APIs for values that would otherwise vary, such as prices, dates, names, and counts.
- Wait for the UI: assert that key content is present and visible. Wait on intercepted requests or application-ready signals instead of arbitrary delays.
- Settle motion: disable animations in test styling or wait for a known completed state. Cypress action-command animation waiting does not ensure that all page animations have ended before a screenshot.
- Mask narrowly: mask or hide only genuinely uncontrolled areas such as a third-party widget, and only if the integration supports it. A small mask is preferable to relaxing the comparison threshold across an entire page.
- Separate themes and states: treat dark mode, responsive layouts, expanded menus, and validation states as distinct checkpoints where they matter.
Visual checks do not establish whether text meets an accessibility contrast standard. Pair them with accessibility checks for requirements that pixel baselines cannot verify.
7. Performance, reliability, and cost
Performance: screenshot capture adds browser work and image comparison adds processing and artifact handling. Keep checkpoints focused and select representative states rather than snapshotting every component in every test. Full-page captures can involve scrolling and stitching. The Cypress documentation’s approximate 100ms capture timing is a technical note, not a performance guarantee; total test time depends on rendering, assets, the plugin, and CI.
Reliability: consistent browser and OS environments reduce pixel noise. Local plugins make your team responsible for that consistency and for storing/reviewing baselines. Hosted services can manage rendering and review infrastructure, but check their supported browsers, data retention, privacy terms, and current plan limits before choosing one.
Cost: many local plugins are open source, but CI compute, artifact storage, and maintenance still have costs. Hosted services generally charge subscriptions; pricing and included browser coverage change, so verify current terms directly. Estimate the number of checkpoints, branches, browsers, and parallel runs your team needs before selecting a plan.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No comparison assertion or command is available | cy.screenshot() was added without a comparison integration, or the integration was not registered. |
Install and configure a compatible plugin or service, import/register its support code, and use its documented assertion. Cypress capture alone only saves screenshots. |
| Module or command not found | Package setup does not match the plugin’s Cypress version, import path, or support-file configuration. | Check the plugin’s current setup instructions, confirm it is installed in the project, and verify the configured support file is loaded. |
| Every run creates a different diff | Dynamic API data, current time, random IDs, animations, third-party content, or rendering environment varies. | Stub changing responses, freeze or control time where appropriate, settle motion, target uncontrolled regions with a narrow mask, and pin browser/OS/viewport. |
| Text wraps differently in CI | Viewport, fonts, browser version, or operating system differs from the baseline environment. | Match the viewport and rendering environment, install the same fonts, and regenerate a baseline only after confirming the layout change is expected. |
| Element is missing or screenshot is blank | The app was captured before mount/data completion, the selector is wrong, or the element is outside the expected state. | Assert the target is visible, wait for the relevant request or ready condition, and confirm the selector resolves to the intended element. |
| Full-page image repeats a header or sticky control | Full-page capture scrolls and stitches while fixed or sticky elements remain on screen. | Use an element or viewport checkpoint if that better fits the assertion, or configure the integration’s supported handling for fixed elements. |
| Diffs fail after a deliberate redesign | The approved baseline still represents the previous design. | Review actual and diff images, then use the integration’s explicit baseline approval/update process and include the changed baseline in review. |
| Threshold changes hide meaningful changes | A broad global tolerance is compensating for unstable rendering. | Stabilize the source of variation first. Keep thresholds conservative and use targeted masks only for truly uncontrolled content. |
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A screenshot API captures a URL directly; it does not replace Cypress assertions or your approved-baseline workflow for testing interactive React states. Use it when you need clean page captures without setting up browser automation. See the ScreenshotNeo API documentation.
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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
- An 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 1,000 free screenshots a month, with no card required.
10. FAQ
Can I test React applications using Cypress?
Yes. Cypress supports both end-to-end and component testing for React. Component tests are useful for controlled visual checkpoints; end-to-end tests are useful when the full application flow is part of the state you need to capture.
Does Cypress compare screenshots by itself?
No. cy.screenshot() captures and saves an image. A visual-diff integration or custom comparison code must compare it with a baseline.
Should every React component have a visual test?
No. Start with representative components and important states where visual regressions would matter. Excessive checkpoints increase maintenance and make review noisier.
Do screenshot comparisons test accessibility?
No. A visual match does not prove accessibility conformance, including text contrast. Pair visual regression checks with accessibility testing.


