How to Use BackstopJS with Storybook
Set up BackstopJS to capture Storybook stories, compare them with approved baselines, and review visual changes across viewports.
Use BackstopJS to capture Storybook stories and compare them against approved screenshot baselines. The key is to point each scenario at the story’s canvas iframe URL, keep the story state and rendering environment stable, and review differences before approving new references.
This guide sets up a local workflow with desktop and mobile viewports, explains how to find story URLs, and covers baseline updates, CI considerations, troubleshooting, and when Storybook’s own testing tools serve a different purpose.
1. Prepare stories for repeatable captures
BackstopJS can only compare what the browser renders. Make each target story render consistently before configuring screenshots:
- Include the providers, decorators, theme, fonts, and assets the component needs.
- Use stable mock data and fixed component states. Avoid current timestamps, random values, and uncontrolled animation.
- Make sure images and fonts are available to the Storybook preview, including in CI.
- Choose the viewport sizes that matter for the component’s design breakpoints.
Storybook stories can depend on context supplied by decorators and preview configuration. See [Storybook setup documentation](https://github.com/storybookjs/storybook/blob/next/docs/get-started/setup.mdx) for that setup.
2. Run Storybook and find a story URL
Start the Storybook development server using the script configured in your project. The current Storybook install documentation gives npm run storybook as the development-server command:
npm run storybook
The default local address is often http://localhost:6006, but use the address and port printed by your project. Open the story you want in Storybook, then use its option to open the canvas in a new tab. The canvas URL commonly follows this shape:
http://localhost:6006/iframe.html?id=components-button--primary&viewMode=story
The value of id is the story ID from your own Storybook build. Do not guess it: confirm it from the canvas URL. Storybook documents embedding stories through its canvas iframe URL format in the (https://storybook.js.org/docs/8/sharing/embed).
3. Install and configure BackstopJS
Add BackstopJS using your project’s package manager, then create a JavaScript configuration file. Confirm package compatibility against the versions used by your project; the version-specific compatibility matrix is not covered here.
npm install --save-dev backstopjs
For an npm script, add a command such as "visual:regression": "backstop" to package.json, then run it with the desired BackstopJS subcommand. Here is a minimal configuration with desktop and mobile captures:
module.exports = {
id: 'storybook-components',
viewports: [
{ label: 'desktop', width: 1280, height: 800 },
{ label: 'mobile', width: 390, height: 844 }
],
scenarios: [
{
label: 'Button / Primary',
url: 'http://localhost:6006/iframe.html?id=components-button--primary&viewMode=story',
selectors: ['document']
}
]
};
Save this as backstop.config.js. Replace the example URL with the canvas URL for a real story in your project. The document selector captures the page content. BackstopJS supports scenario configuration for selectors, waits, scripts, and viewport settings; see the [BackstopJS README](https://github.com/garris/BackstopJS) for the options available to your installed version.
Choose what each scenario captures
- One scenario per state: give each meaningful story state a distinct label and URL. A primary button and a disabled button should be separate scenarios.
- One or more viewports: each scenario is captured at each configured viewport, so adding viewports increases the screenshot count and time needed to review.
- Whole document or a selector: capture the full preview when the page context matters; use a narrower selector when surrounding layout is irrelevant and the target is stable.
- Waits and scripts: use BackstopJS readiness controls or custom scripts for stories that need time or interaction. Base waits on the actual readiness condition, not an arbitrary long delay.
BackstopJS does not automatically discover all Storybook stories or generate a complete project config. If you want that behavior, add and maintain project-specific discovery code.
4. Capture references, compare, and approve changes
With Storybook reachable and the config in place, use BackstopJS’s documented reference and test workflow:
npx backstop reference --config=backstop.config.js
npx backstop test --config=backstop.config.js
npx backstop approve --config=backstop.config.js
- Reference: captures the initial screenshots used as baselines.
- Test: captures the current output and compares it with the stored references.
- Review: open the generated browser report and inspect each difference at each relevant viewport.
- Approve: run this only after confirming the changes are intended. It promotes the latest test images to the reference collection.
A visual difference is a prompt to inspect the result, not proof of a bug. Keep baseline updates deliberate: approve only the scenarios and viewports whose changes you have reviewed.
5. Run visual checks in CI
CI needs to serve the same Storybook build that BackstopJS will visit. You can start the development server as part of the job or build and serve Storybook’s static output. In either case:
- Wait for the server to be ready before starting BackstopJS.
- Use the CI server’s reachable hostname and port in the scenario URLs;
localhostonly works when the browser process shares the same environment. - Keep browser, operating-system, viewport, mock data, and assets consistent between reference creation and comparisons.
- Store and review generated reports and test screenshots as CI artifacts according to your project’s workflow.
- Separate intentional reference updates from ordinary test runs so a failing visual check cannot silently approve its own changes.
BackstopJS lists Docker rendering as one way to reduce cross-platform rendering differences. It does not guarantee pixel-identical output in every environment, so keep the rendering setup as consistent as practical.
6. Keep captures stable and manageable
Stability
Visual comparison works best when the page reaches the same state on every run. Control dynamic content, mock network-dependent data, ensure fonts and images finish loading, and disable or stabilize animations where they affect the captured result. Use readiness checks tied to the component’s actual state. A fixed delay may help with a known transition, but excessive delays make runs slower without fixing a race condition.
Performance and review effort
The number of captures grows with scenarios and viewports: every scenario is checked at each configured viewport. Start with the stories and breakpoints that protect important components, then expand when the reports are still practical to review. Narrow captures can reduce irrelevant differences, but they should not hide layout changes the test is meant to catch.
Reliability and cost
BackstopJS is a screenshot baseline workflow you configure and run with your project. Plan for the work of maintaining story scenarios, rendering dependencies, and reference images. The research used for this guide does not establish a benchmark, hosted-service price, or test duration, so those depend on your project and environment.
7. BackstopJS and Storybook testing tools
BackstopJS compares screenshots over time. Storybook Test Runner visits stories in a running Storybook instance and checks rendering failures and play-function assertions; that addresses related but different needs. Storybook’s current [Test Runner page](https://storybook.js.org/addons/@storybook/test-runner) says official support for Test Runner has ended and suggests that Vite-based projects consider Storybook’s Vitest integration. Check support and compatibility for your exact Storybook version before adopting or retaining a runner.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Story URL does not load | Storybook is not reachable, the port differs, or the story ID is missing from the running build. | Check the server address and open the story’s canvas in a new tab. Copy that URL into the scenario. |
| Story looks different from the Storybook manager | The canvas is missing a provider, decorator, font, asset, or runtime setup expected by the component. | Review preview configuration and decorators, and ensure the preview has access to required assets and context. |
| Captures vary between runs | Uncontrolled data, animation, late-loading assets, or a readiness race changes the captured state. | Stabilize data and animation, ensure assets are ready, and use a readiness control or script based on the real page state. |
| Large numbers of differences appear after a machine or browser change | Rendering environment differences affect pixels, even when component code did not change. | Compare in a consistent environment and consider the Docker rendering option documented by BackstopJS. |
| A test report shows a difference after a design update | The baseline still reflects the previous appearance. | Inspect each affected scenario and viewport. Approve only the reviewed, intended updates. |
| CI cannot reach the scenario URL | The config points to a local address that is not reachable from the browser process, or Storybook is not ready. | Use an address reachable inside the CI job and wait for Storybook to start before invoking BackstopJS. |
Or skip the browser setup
If you need a screenshot of a public page without maintaining a browser capture environment, ScreenshotNeo provides a website screenshot API and MCP server. Its API can return an image or PDF from one GET request. See the ScreenshotNeo API documentation for configuration and options.
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}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/).
FAQ
Does BackstopJS discover Storybook stories automatically?
No complete automatic config is described here. Add scenarios for the stories you need, or write project-specific discovery code.
Can I use a Storybook manager URL as the scenario target?
Use the canvas iframe URL for the story preview. Open the canvas in a new tab to confirm the correct URL and story ID.
Should every visual difference fail the build?
Treat differences as review signals. Decide whether each change is intended before updating references or treating it as a defect.
Can BackstopJS replace interaction and assertion tests?
Screenshot comparison and play-function or rendering assertions cover different failure modes. Use the checks that match what you need to validate.


