How to Run Screenshot Comparison Tests with BackstopJS
Set up BackstopJS visual regression tests, compare captures with approved baselines, review diffs, and keep runs reliable in CI.
BackstopJS runs visual regression tests by capturing your pages and comparing the new screenshots with approved reference images. Install it, initialize a project, define viewports and scenarios, run backstop test, review the report, and run backstop approve only when the changes are intentional. The approved captures then become the references for later tests.
This guide covers a repeatable local workflow and the choices that matter for reliable team and CI runs. BackstopJS checks visual output; it does not replace functional assertions.
1. Install BackstopJS and initialize a project
BackstopJS can be installed globally or as a project dependency. A project-local install keeps the dependency associated with the application and lets the team use the same version through its package scripts.
# Global installation
npm install -g backstopjs
# Or install it in the project
npm install --save-dev backstopjs
From the project directory, initialize the configuration and supporting files:
npx backstop init
If you installed globally, use backstop init. Initialization can overwrite existing files, so inspect the target directory first, especially in an established project. The default configuration is backstop.json in the project root.
BackstopJS also supports a JavaScript configuration file, which is useful when you want comments or need to construct configuration values. Select a non-default config file consistently for both testing and approval with --config=<path>.
2. Define viewports and scenarios
A minimal configuration has an id, at least one viewport, and one or more scenarios. Each scenario needs a label and URL. Choose scenarios that represent repeatable user-visible states, and choose viewport dimensions that cover the layouts your team needs to protect.
{
"id": "webapp",
"viewports": [
{ "label": "desktop", "width": 1440, "height": 900 },
{ "label": "mobile", "width": 390, "height": 844 }
],
"scenarios": [
{
"label": "home",
"url": "http://localhost:3000/"
},
{
"label": "pricing",
"url": "http://localhost:3000/pricing"
}
]
}
Save this as backstop.json. The URLs may be absolute or local to the project. Start the application before running the suite if scenarios point at a local development server.
Plan coverage around states, not URL count
A useful scenario represents a page state that should remain visually stable: for example, a landing page, a pricing page, or a signed-in view after an expected interaction. A long list of URLs can still miss important states if each capture lands before content loads or fails to represent the intended user flow.
For pages that need authentication or interaction, consult the scenario-property documentation in the BackstopJS repository. The project documentation describes support for concerns such as cookies, selectors, and interactions; configure them deliberately rather than assuming a default page load will reproduce the required state.
3. Capture screenshots and compare them
Run the test command from the project directory:
npx backstop test
BackstopJS captures test bitmaps and compares them with the current reference set. The command presents a visual report so you can inspect the reference, test, and difference images. On the first run, initialize the reference set as directed by the generated project workflow; after a reference exists, each test compares against that accepted set.
To rerun only matching scenarios, pass a regular expression filter:
npx backstop test --filter="pricing"
# Example: rerun a subset matching either label
npx backstop test --filter="home|pricing"
Filtering is useful for investigating a failure or iterating on one scenario. Run the full suite before relying on the result for a complete change review.
4. Review diffs and approve intentional changes
For every difference, inspect the old reference, new test capture, and diff. Decide whether it is a defect, an unstable capture, or an intentional design change. If the change is intended, promote the new capture:
npx backstop approve
Approval changes the baseline used by future runs. Treat it as a review decision, not a way to make a failing command green. The command also supports filtering selected image files. If you tested with a custom config file, pass the same config when approving:
npx backstop approve --config=./config/backstop.json
For team review, include changed reference images in version control alongside the code change and explain why the visual difference is expected. This makes future comparisons traceable and lets reviewers see what the new accepted appearance is.
5. Choose rendering, tolerance, and concurrency settings
Use Docker when consistency across machines matters
The project documents an optional Docker rendering mode:
npx backstop test --docker
Using a standardized rendering environment can reduce variation between developer machines and CI workers. It cannot guarantee identical captures in every case: application data, external resources, timing, fonts, and other sources of nondeterminism can still affect output.
Set mismatch tolerance based on observed noise
misMatchThreshold is a percentage tolerance for image differences before a screenshot is marked failed. There is no universal best value. Browser rendering, fonts, animation, dynamic content, and the amount of difference your team is willing to review all affect the right setting.
First stabilize the page and inspect representative diffs. Raise tolerance only when the remaining differences are understood and acceptable; a high threshold can hide meaningful regressions.
Control capture and comparison concurrency
BackstopJS exposes separate limits for image capture and image comparison: asyncCaptureLimit and asyncCompareLimit. If the suite runs out of memory, reduce concurrency. If the CI worker has spare capacity and runtime matters, adjust the limits while watching worker resource use. The package documentation’s RAM estimate is approximate, so use your own runner behavior to guide tuning rather than treating it as a guarantee.
6. Make CI runs repeatable
- Install the project’s locked dependencies so local and CI runs use the intended BackstopJS version.
- Start the application and any required test data or authentication setup before capture.
- Use stable scenario URLs and state, and avoid depending on changing external content where possible.
- Run
backstop testin the same rendering mode across runs; use Docker if a consistent browser environment is important to your workflow. - Publish or retain the visual report and diff artifacts where reviewers can inspect failures.
- Approve baselines only after reviewing the images, and commit the updated references with the change they represent.
Keep visual assertions separate from behavioral checks. A screenshot may look correct even when a control is broken, and a functional test may pass while a layout regresses.
7. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Initialization replaces an existing file | backstop init writes scaffold files into the current directory. |
Check the directory before initialization and preserve existing configuration or assets before regenerating scaffolding. |
| Local URL cannot be captured | The application server is not running, the URL or port is wrong, or the CI job starts capture too early. | Start the app before BackstopJS, verify the exact URL from the runner environment, and ensure the server is ready before capture. |
| Every run reports unexpected differences | The rendered state is changing because of animation, dynamic data, timing, fonts, or environment variation. | Make the scenario state repeatable, inspect the diff to identify the changing region, and consider Docker rendering for environment consistency. Adjust tolerance only after identifying acceptable noise. |
| Test passes but the expected baseline did not change | Tests do not automatically approve their captures. | Review the report, then run backstop approve for the intentional changes. |
| Approval uses the wrong config or images | The approval command is pointed at a different configuration or filter than the test. | Use the same --config file and confirm the selected images before approving. |
| Suite is too slow | Many scenarios and viewports increase capture and comparison work; concurrency may be conservative. | Use filters while debugging, keep scenarios focused on important states, and tune asyncCaptureLimit and asyncCompareLimit against the CI worker’s capacity. |
| Runner runs out of memory | Concurrent captures or comparisons exceed available worker resources. | Lower capture and comparison concurrency, then rerun the suite and monitor memory use. |
| A visually changed page has no useful test coverage | The scenario may capture the wrong state or omit a needed viewport or interaction. | Add a scenario for the relevant user-visible state and configure required cookies, selectors, or interactions using the project’s scenario documentation. |
8. Or skip the browser setup
If you want a screenshot without maintaining a browser runner and reference-image workflow, ScreenshotNeo returns a screenshot from one API request. See the API documentation for request options.
# cURL
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)
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}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The Free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. These are on-demand captures, not a replacement for BackstopJS’s approved-baseline comparison workflow.
Sign up for 1,000 free screenshots a month, with no card required.
9. FAQ
Does BackstopJS replace end-to-end tests?
No. It detects rendered visual differences. Keep functional tests for behavior such as navigation, form submission, and authorization.
Should every screenshot difference fail CI?
Use the configured tolerance and review process to distinguish meaningful regressions from known rendering noise. A difference should be investigated before it is accepted or suppressed.
Can I test only one scenario while debugging?
Yes. Use --filter=<scenarioLabelRegex> with backstop test, then run the full suite before merging if complete coverage is required.
When should I approve a new reference?
After confirming the captured change is intentional and reviewing the affected images. Approval sets the reference future runs will compare against.


