React Screenshot Testing: Capture and Compare UI Changes
Build reliable React visual regression tests with Playwright or Storybook and Chromatic. Capture deterministic states, inspect pixel diffs, and update baselines safely.
React screenshot testing captures a rendered interface and compares it with an approved image. The comparison reveals visual changes such as shifted layouts, altered colors, missing content, or typography differences. For a browser route or journey, Playwright Test provides toHaveScreenshot(); for reusable component states, Storybook stories combined with Chromatic provide a hosted review workflow. A pixel difference is a signal to review, not proof of a bug: it may be an unintended regression, an intentional design change, or an environment difference.
This guide builds a repeatable Playwright visual test, explains the Storybook and Chromatic option, and covers baseline management, capture stability, CI, troubleshooting, and cost considerations.
1. Choose the test scope
| Workflow | Use it for | Baseline and review |
|---|---|---|
| Playwright Test | React routes, full pages, and selected points in an end-to-end journey | Reference screenshots are stored with the test snapshots; review and update them in version control. |
| Storybook with Chromatic | Repeatable component and design-system states already represented as stories, with hosted review | Chromatic captures stories and shows changed stories and pixels for acceptance or correction. |
They can complement each other. Storybook documents isolated states, while Playwright exercises application routes and user journeys. Storybook documents reusing stories in Playwright or Cypress end-to-end tests. See the Storybook testing guide.
2. Set up Playwright for a React app
Install Playwright Test and its browser. The setup command creates a configuration and example test files; choose TypeScript if prompted.
npm init playwright@latest
npx playwright install
Ensure the test configuration starts the React development or preview server, or run that server separately before the tests. A typical configuration is:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://127.0.0.1:4173',
...devices['Desktop Chrome'],
},
webServer: {
command: 'npm run preview -- --host 127.0.0.1',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI,
},
});
For a Vite app, build it before running this preview configuration. Adapt the server command and URL to the project. For development-server testing instead, use the project’s development command and its port. Keep the browser, operating system, fonts, and rendering conditions consistent between baseline creation and CI.
3. Capture and compare a React page
Create tests/visual.spec.ts. This complete test visits the home route, waits for an app-specific stable marker, then compares a full-page screenshot:
import { test, expect } from '@playwright/test';
test('home page visual baseline', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('main')).toBeVisible();
await expect(page).toHaveScreenshot('home-page.png', {
fullPage: true,
});
});
Replace the marker or route with selectors and paths that exist in your application. On the first run, Playwright creates the reference screenshot. Run the test again to compare against it:
npx playwright test tests/visual.spec.ts
Playwright’s toHaveScreenshot() waits for two consecutive screenshots to match before comparison, helping avoid captures while rendering is still settling. Baselines are associated with browser and platform; if browser or platform coverage differs, plan for corresponding references. Consult the Playwright visual comparisons documentation for snapshot behavior and options.
Capture a specific component
Use a locator screenshot when the component is the risk under test, such as a navigation menu or pricing card. Keep the locator specific so the assertion captures the intended region.
import { test, expect } from '@playwright/test';
test('pricing card appearance', async ({ page }) => {
await page.goto('/pricing');
const card = page.getByTestId('pro-plan-card');
await expect(card).toBeVisible();
await expect(card).toHaveScreenshot('pro-plan-card.png');
});
Capture a named UI state
Visual tests are more useful when they explicitly establish the state to capture. For example, open a menu before taking its image:
test('navigation menu open', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Open menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page).toHaveScreenshot('navigation-open.png');
});
Use accessible roles and names when practical. If the UI has multiple navigation regions, use a more specific locator. For authenticated or data-dependent routes, establish the same user and data state for every run, for example with deterministic test fixtures or mocked responses.
4. Review diffs and update baselines deliberately
When a comparison fails, inspect the actual image, expected baseline, and generated diff. Decide whether the change is a defect, intended UI work, or capture noise. Fix the cause or approve the design change, then update the reference screenshots:
npx playwright test --update-snapshots
Review and commit the changed baseline files alongside the relevant code change. Avoid accepting every changed image without inspection: a baseline update can otherwise encode a regression as the new expected appearance.
Playwright supports comparison configuration, including maxDiffPixels, and a capture-time stylesheet through stylePath. Use a threshold only for a known, low-value rendering variation. A broad tolerance can conceal real layout or styling changes. A stylesheet can hide a volatile element, but hide only content that is outside the test’s purpose.
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 40,
stylePath: './tests/visual-stability.css',
});
/* tests/visual-stability.css */
/* Hide a known volatile timestamp only if it is not under test. */
[data-testid="last-updated"] {
visibility: hidden !important;
}
5. Use Storybook stories with Chromatic
If the important states already exist as Storybook stories, the official Storybook visual testing addon, @chromatic-com/storybook, turns those stories into visual tests. Each story acts as a repeatable component-state case; Chromatic captures it and presents changed stories for review. Storybook recommends running the addon during development and Chromatic in CI before merge. See Storybook visual testing and Chromatic snapshots.
Install and configure the addon using the current Storybook instructions for your version, then connect the project to Chromatic and configure an authenticated CI run. No project token or workflow command is included here because the correct values and setup depend on your Chromatic project. Keep your stories deterministic: provide fixed args and fixtures, avoid uncontrolled clocks and network data, and ensure interactions finish before capture.
Chromatic documents capture inputs that include Storybook stories, Vitest browser-mode tests, and Playwright and Cypress end-to-end tests. Its capture flow loads tests in a selected device and viewport, waits for rendering, then compares screenshots against the prior baseline. Chromatic pauses CSS animations and transitions, videos, and GIFs during capture; JavaScript-driven animation still needs to be controlled by the test author. Device pixel ratio (DPR) is part of the image: changing DPR can produce a visual change and require reviewed baselines.
6. Make captures deterministic
- Control data: seed or mock network responses, use stable fixtures, and avoid random values.
- Wait for the right state: wait for a meaningful element or completed interaction rather than relying on an arbitrary short delay.
- Stabilize time: freeze or set clocks when dates, countdowns, or time-sensitive content appear.
- Handle animation: disable or finish CSS and JavaScript animations where they are not the behavior being tested. Do not assume every capture tool controls JavaScript animation.
- Choose the capture boundary: a viewport, named element, and full page answer different questions. Keep the choice consistent.
- Keep rendering conditions stable: browser version, OS, fonts, viewport, DPR, and headless settings can change pixels. Playwright specifically warns that host OS, browser, settings, hardware, power source, and headless mode can affect screenshots.
- Keep relevant content visible: mask or hide only known volatile areas outside the test’s intent. A date label may be noise in a layout test, but content is not noise if its display is part of the requirement.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| First run creates files instead of failing | No approved baseline exists yet. | Inspect the generated image, then commit it as the initial reference if it is correct. |
| Many pixels differ on CI but not locally | Different browser, OS, fonts, DPR, headless configuration, or hardware rendering. | Align the baseline and test environment; regenerate only after reviewing the change. |
| Screenshot shows a loading state | The page was captured before the relevant data or component settled. | Wait for an app-specific visible state, or control the response with stable test data. |
| Flaky differences around timestamps or rotating content | Uncontrolled clocks, random data, carousels, ads, or external network content. | Freeze time, seed or mock data, or hide a specific irrelevant region with a capture stylesheet. |
| Diff appears after a browser or dependency upgrade | Rendering changed across browser, platform, or library versions. | Review the diff as a potential environment migration; update references intentionally and keep the version change explicit. |
| Animation appears at inconsistent frames | JavaScript animation or transitions are still running at capture time. | Set the component to a fixed state or disable the animation for the test. Verify the selected tool’s capture behavior. |
| Diff is noisy across an entire page | Capture boundary includes irrelevant dynamic regions, or layout is not settled. | Capture a specific locator, wait for layout completion, and narrowly exclude only known volatile content. |
| Baseline update unexpectedly changes many files | Command updated all snapshots affected by the run, or test configuration changed. | Review the diff by test and platform, revert unrelated changes, and run only the intended test when regenerating. |
8. Performance, reliability, and cost
Visual tests add browser rendering and image comparison to the test workflow. Keep the suite focused on representative high-risk routes and states, reuse setup where safe, and avoid capturing the same unchanged surface repeatedly. Full-page captures and large numbers of component states naturally create more images and review work. Parallel execution can reduce elapsed time but consumes more runner resources; measure within your own CI environment rather than relying on a universal speed estimate.
Reliability comes from reproducible inputs and rendering conditions, not simply from loosening pixel thresholds. Treat image diffs as review artifacts, keep baselines versioned or hosted with clear ownership, and make baseline updates visible in code review. Chromatic adds a hosted capture and review workflow that requires connecting a project and configuring CI; evaluate its current plan and usage terms directly, since this research dossier does not establish current pricing. Playwright’s local screenshot assertions use the project’s test and CI infrastructure. Neither workflow removes the need to inspect meaningful visual changes.
9. Distinguish image comparisons from markup snapshots
An image-based visual test compares rendered pixels and can reveal appearance changes even when the DOM structure is unchanged. A markup snapshot compares serialized markup and can detect structural changes that may not be visible. Neither replaces behavioral assertions: use interaction tests to verify outcomes such as navigation, validation, or state updates. Storybook distinguishes visual tests from its markup snapshot testing in its visual testing guide and snapshot testing documentation.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request captures a URL as PNG, JPEG, WebP, or PDF. This is useful when you need a page capture without setting up browser automation; it does not replace Playwright’s in-test assertions against your React app’s controlled state.
See the ScreenshotNeo API documentation for request options. This runnable cURL example saves a WebP screenshot:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
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 Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. For React visual regression, keep your Playwright or Storybook baseline workflow; use the API when a clean website capture is the task. Sign up for 1,000 free screenshots a month with no card.
Frequently asked questions
Does a visual diff mean my React UI is broken?
No. A diff means the rendered image changed. Review it to determine whether the change is intended, a defect, or caused by rendering conditions.
Should every component have a screenshot test?
Prioritize states where visual regressions matter and where a stable, representative story or route can be maintained. More captures also mean more baselines and review effort.
Can I use both Playwright and Chromatic?
Yes. Use Chromatic for story-based component states and Playwright for application routes or journeys that need browser interaction; Storybook documents story reuse in end-to-end tests.
When should I update a baseline?
After inspecting the changed image and confirming the new appearance is intended. Include the updated reference with the UI change so reviewers can assess both together.


