How to Do Visual Testing for React and Storybook
Add repeatable visual regression checks to React stories: capture representative states, compare them with baselines, review changes, and run checks in CI.
To do visual testing for React and Storybook, make stories for the component states that matter, capture their rendered appearance, and compare those captures with an approved baseline. For a Storybook-first hosted workflow, Storybook documents the @chromatic-com/storybook addon: once enabled, stories become visual tests, and you can review diffs during development and run checks in CI. A visual diff checks rendered pixels; it does not prove that application behavior or markup is correct. Keep interaction, accessibility, and logic checks alongside it. Storybook visual testing guide.
1. What visual testing checks
A visual regression check takes a screenshot of a rendered UI state and compares it with a known image baseline. It can reveal changes in layout, color, size, contrast, typography, spacing, or other visible details. A markup snapshot compares HTML output instead; HTML can change without a visible difference, and a rendered difference can be caused by styles or assets without a meaningful markup change.
In Storybook, each story is a reusable, isolated description of a UI state. That makes stories useful visual test cases: the same component can be checked with different props, content, themes, and interaction states. A screenshot diff is a review signal, not an automatic verdict. Investigate unexpected changes, fix regressions, and approve intentional design changes as new baselines.
2. Prepare representative React stories
Begin with the states whose appearance matters to users or downstream teams. Include the ordinary state and meaningful variations; add edge states when they expose distinct layout risks.
- Typical content and the component’s primary visual state.
- Long, empty, loading, error, or disabled content when those states are part of the component.
- Important prop variants, such as size, emphasis, or status.
- Interaction states, such as open menus or selected tabs, when represented by stories.
- Relevant theme or viewport variants when the team needs to catch differences in them.
Keep story data and rendering conditions repeatable. Avoid relying on the current time, random values, remote data, user-specific settings, or animations that can change between captures. Do not create a story for every theoretical combination: choose states that exercise distinct visual behavior and are useful to review.
Example component and stories
This minimal React example defines stable default, long-content, and disabled states. Put the files in your project’s usual component and Storybook locations; adapt imports and styling to your setup.
// src/components/Notice.tsx
import type { ReactNode } from 'react';
export type NoticeProps = {
title: string;
children: ReactNode;
tone?: 'info' | 'warning';
disabled?: boolean;
};
export function Notice({ title, children, tone = 'info', disabled = false }: NoticeProps) {
return (
<section aria-disabled={disabled} className={`notice notice--${tone}`}>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
// src/components/Notice.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Notice } from './Notice';
const meta = {
title: 'Components/Notice',
component: Notice,
args: {
title: 'Service update',
children: 'Your settings have been saved.',
},
} satisfies Meta<typeof Notice>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WarningWithLongContent: Story = {
args: {
tone: 'warning',
title: 'Scheduled maintenance',
children:
'Some features may be unavailable while maintenance is in progress. Save your work before continuing and check back later for updates.',
},
};
export const Disabled: Story = {
args: {
disabled: true,
title: 'Read-only notice',
children: 'This message cannot currently be changed.',
},
};
Stories should render the state being tested without depending on hidden setup. If a story uses decorators, providers, fonts, or global styles, make sure those are included consistently in local Storybook and CI.
3. Enable Storybook visual tests with Chromatic
Storybook’s documented native visual-testing route uses Chromatic, a cloud service from the Storybook team. The official addon is @chromatic-com/storybook; the visual-testing guide lists Storybook 7.6 or later as a prerequisite. Check the current guide for compatibility with your framework and version before installing.
- Confirm your Storybook version and framework are compatible with the current addon.
- Run the Storybook CLI setup command from the project root.
- Sign in to Chromatic and create or select a project when prompted.
- Start Storybook and open its Visual Tests panel to capture and review stories.
- Run the project build and visual workflow in CI before merging changes.
npx storybook@latest add @chromatic-com/storybook
npm run storybook
The CLI configures the addon and project identifiers. Add the generated configuration to source control as appropriate for your project. Storybook’s guide documents chromatic.config.json options including projectId, buildScriptName, debug, and zip. The project ID is normally configured by setup; a custom build script can be named with buildScriptName, debug: true enables verbose output, and zip: true is recommended there for large projects.
{
"projectId": "Project:YOUR_PROJECT_ID",
"buildScriptName": "build-storybook",
"debug": false,
"zip": true
}
Use the project ID generated for your own Chromatic project rather than copying the placeholder. Keep credentials such as project tokens in CI secrets, not in committed files. See Storybook’s setup, configuration, review, and CI instructions.
4. Establish and review baselines
The first accepted capture establishes the reference image for a story. Later captures are compared with that reference. Use the development loop deliberately:
- Run visual tests for the stories in scope.
- Inspect each changed story and its highlighted pixel differences.
- Decide whether the difference is intended by the code or design change.
- If intended, accept the updated baseline so the team shares the new reference.
- If unexpected, fix the component, styling, story fixture, or capture conditions and rerun.
Do not accept a batch of changes without reviewing them. A font failing to load, a missing image, a shifted layout, and a planned redesign can all produce diffs but need different responses. Once reviewed locally, commit the code and let CI check the branch against the synchronized approved baseline.
5. Run checks in CI
Storybook recommends using its visual-testing addon during development and running visual checks in CI before merge. CI makes the check repeatable for the branch and gives reviewers a place to see whether visual changes need approval. Configure the current Chromatic CI command or provider integration from the official setup guide, and store the project token in the CI system’s secret store. The exact workflow syntax depends on your CI provider and project scripts.
A practical merge gate should make the visual result visible to reviewers and require review of unexpected differences. A failed or changed screenshot is evidence to investigate, not automatically a defect. Keep baseline approval tied to a reviewed design or code change.
6. Choose the right testing layer
| Need | Useful approach | What it answers |
|---|---|---|
| Isolated component appearance across representative states | Storybook stories with visual tests | Did this story’s rendered appearance change? |
| Rendering, interaction, accessibility, or component behavior | Storybook testing tools and browser tests | Does the component render and behave as expected? |
| A complete user journey across application routes | Playwright E2E plus visual snapshots | Did the end-to-end page or journey look different? |
| Reusable UI state in other test suites | Import stories into Playwright, Cypress, Vitest, or Jest where supported | Can existing tests reuse the story’s props and fixtures? |
Storybook documents its Test experience as transforming stories into Vitest tests through browser mode. Its current guidance recommends the Vitest addon for Vite-powered Storybook frameworks; the older Storybook test-runner has been superseded for that path. The Vitest addon is Vite-dependent, so check the migration guide for framework and builder compatibility. Stories can still be useful as reusable fixtures in other test environments; that is separate from enabling hosted visual comparisons. Vitest addon guide · Test-runner guidance.
For journey-level visuals, Chromatic documents a Playwright integration that extends Playwright’s test and expect utilities, captures test states, and sends archives to its cloud for snapshot generation and pixel comparison. The documented method requires Chrome in the Playwright configuration. Its documentation says TurboSnap is incompatible with this black-box Playwright method. Use this route when the visual state depends on navigation or an end-to-end flow rather than a single isolated story. Chromatic Playwright setup.
7. Make captures repeatable
Visual checks are useful only when a repeated run of unchanged code produces a comparable image. Control the inputs that affect pixels:
- Data: use fixed story arguments and fixtures; avoid random or time-dependent content.
- Fonts and assets: ensure local fonts and images have loaded before capture. Missing or late assets can shift text and layout.
- Animation: disable or settle transitions and animated content for the capture state where possible.
- Environment: keep browser, viewport, theme, locale, and relevant browser settings consistent with the coverage you intend.
- Network: avoid dependencies on unstable third-party resources; provide deterministic local fixtures when practical.
- Story scope: add meaningful visual states, then remove redundant variants that do not change what reviewers need to verify.
When a change appears only in one browser, viewport, or locale, preserve that dimension in the test matrix if it matters to users. Avoid multiplying every story by every possible environment without a reason: review volume and execution cost grow with the number of captured states.
8. Troubleshooting common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| Addon command or setup fails | Storybook version or framework is outside the supported setup. | Check the current official prerequisite and migration documentation; update the compatible Storybook setup before retrying. |
| Story renders differently in CI | Different fonts, assets, environment variables, viewport, theme, or browser conditions. | Compare CI and local inputs; make story fixtures and global providers explicit, and confirm fonts and images are ready. |
| Many unrelated pixels change | Layout shift, missing asset, font fallback, animation, or unstable content. | Inspect the baseline and new image at full context; stabilize the underlying input before accepting any baseline. |
| Only one story fails or changes | That story may rely on a missing provider, decorator, or data condition. | Open it directly in Storybook, inspect its console and dependencies, and make the state self-contained. |
| Visual workflow cannot find a project | Project setup or identifier was not completed or the wrong project is selected. | Reopen the addon setup and verify the generated project identifier against the intended Chromatic project. |
| Vite/Vitest addon will not run | The project uses a builder or framework not supported by the Vitest addon. | Check the official compatibility guidance. Keep the compatible runner or migrate the Storybook framework before switching. |
| Playwright visual archive fails | Browser setup is incomplete, or incompatible project overrides/dependencies affect the integration. | Install/configure Chrome as the integration requires and follow its current troubleshooting guidance for dependency conflicts. |
| Diff appears but may be a desired redesign | Baseline is older than the intended design. | Have the change reviewed, then accept the new baseline; otherwise fix the regression and recapture. |
9. Performance, reliability, and cost
The main workload drivers are the number of stories captured and the number of environments, viewports, themes, or browsers tested. Start with important states and the coverage the product needs. Broader matrices catch more environment-specific differences but create more images to generate and review. Storybook’s configuration guide documents a zip option for large projects; follow current provider guidance for service usage limits and prices, since setup documentation does not establish a universal cost.
For reliability, use stable fixtures, consistent capture settings, and a baseline approval process. Treat repeated unexplained changes as a capture reproducibility problem to diagnose. Keep functional and accessibility checks too: a pixel comparison cannot establish that a button works, that content is semantically correct, or that a change is accessible.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It can capture a published Storybook story or other public page as an image; it does not replace a visual-regression baseline and diff workflow. Its capture options include full-page screenshots, element selection, device and viewport settings, dark mode, custom CSS and JavaScript, waiting for a selector or network idle, and image formats including PNG, JPEG, and WebP. See the ScreenshotNeo API documentation.
One GET request captures the page:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-storybook.example.com/?path=/story/components-notice--default -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://your-storybook.example.com/?path=/story/components-notice--default",
},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://your-storybook.example.com/?path=/story/components-notice--default',
});
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', res);
Use a URL that the capture service can reach. A Storybook running only on localhost or behind authentication is not publicly reachable unless you expose it through an appropriate deployment or configure access through supported request options. For private preview deployments, check the docs for supported headers and authentication. The response can identify page verdict and billing information in response headers; inspect those when automating captures.
Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Use the API to capture pages, then retain your chosen baseline comparison and human review process.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a visual test replace a React unit test?
No. It checks rendered appearance against an image baseline. Keep tests for behavior, logic, and accessibility alongside it.
Should every story be captured?
Storybook’s documented Chromatic integration can turn every story into a visual test. Choose stories deliberately so the set represents meaningful states without producing redundant review work.
Can I use Playwright instead of Storybook visual tests?
Yes, particularly when the appearance belongs to a complete journey. Use Storybook stories for isolated component states and Playwright when navigation or application-level setup is part of the visual state.
Is accepting a diff the same as fixing a bug?
No. Accept a diff only after review confirms the new appearance is intended; otherwise correct the cause and run the capture again.


