Getting Started with BackstopJS: Visual Regression Testing for Web Apps
Visual regression testing catches the bugs that unit tests and integration tests never see — a misaligned button, a font that loaded wrong, a layout that collapsed on tablet. BackstopJS is the most popular open-source tool for this job. It takes screenshots, compares them to approved baselines, and fails your build when something shifts.
This guide walks you from zero to a working visual regression suite in a single session.
What BackstopJS Does
BackstopJS automates the screenshot-and-diff cycle. You define scenarios (URLs + interaction steps), run backstop test, and it produces an HTML report showing pixel-level diffs for every scenario that changed. When the change is intentional, you run backstop approve to update the baseline. When it's a regression, you fix the bug.
The tool supports two rendering engines: Puppeteer (default, Chromium-based) and Playwright (multi-browser). For most teams starting out, Puppeteer is fine.
Installation
BackstopJS requires Node.js 14 or later. Install it globally or as a dev dependency — both work, but a local install is easier to version-lock in a team project.
# Global install
npm install -g backstopjs
# Or as a dev dependency (recommended)
npm install --save-dev backstopjsFor the Playwright engine (optional, needed for Firefox/WebKit):
npm install --save-dev backstopjs
npx playwright installVerify the install:
npx backstop --versionInitializing a Project
Inside your project root, run:
npx backstop initThis creates two things:
backstop.json— your configuration filebackstop_data/— directory for engine scripts, bitmaps, and HTML reports
The generated backstop.json includes a sample scenario pointing to https://garris.github.io/backstopjs-tutorial/. Replace it with your own URLs before running anything.
Your First backstop.json
Here is a minimal but real configuration:
{
"id": "my_project",
"viewports": [
{ "label": "desktop", "width": 1280, "height": 800 },
{ "label": "mobile", "width": 375, "height": 812 }
],
"onBeforeScript": "puppet/onBefore.js",
"onReadyScript": "puppet/onReady.js",
"scenarios": [
{
"label": "Homepage",
"url": "https://your-app.example.com/",
"delay": 500,
"misMatchThreshold": 0.1
},
{
"label": "Login Page",
"url": "https://your-app.example.com/login",
"delay": 300,
"misMatchThreshold": 0.1
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"engine_scripts": "backstop_data/engine_scripts",
"html_report": "backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": ["browser", "CI"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--no-sandbox"]
},
"asyncCaptureLimit": 5,
"asyncCompareLimit": 50,
"debug": false,
"debugWindow": false
}Key fields:
id— used as a prefix for reference bitmap filenames; change it and all baselines are invalidatedviewports— each scenario is tested at every viewport; two viewports means two screenshots per scenariomisMatchThreshold— percentage of pixels allowed to differ before the test fails;0.1means 0.1%delay— milliseconds to wait after page load before capturing; useful for animations and lazy-loaded images
Running Your First Test
Before you can compare anything, you need a baseline. Create it with:
npx backstop referenceThis loads each scenario URL, waits for delay milliseconds, and saves a PNG to backstop_data/bitmaps_reference/. You should commit these images to version control — they are the ground truth your team agrees on.
Now run the test:
npx backstop testBackstopJS opens each URL again, takes new screenshots, and compares them pixel-by-pixel to the reference images. Since nothing has changed, every scenario should pass.
Open the HTML report to confirm:
npx backstop openReportThe report shows a side-by-side diff for each scenario and viewport. Green border = pass, red border = fail.
Approving Baselines After Intentional Changes
When you update your UI on purpose — a redesign, a new component — the test will fail because the screenshots no longer match the old baseline. That is correct behavior. After verifying the changes are intentional, update the baselines:
npx backstop approveThis copies the latest test screenshots over the reference images. Commit the updated references to your repository so teammates and CI see the same baselines.
You can approve a single scenario instead of all of them:
npx backstop approve --filter="Homepage"The --filter flag accepts a substring match against the scenario label.
Useful CLI Flags
# Run only scenarios matching a label substring
npx backstop test --filter="Login"
# Run reference capture for a subset
npx backstop reference --filter="Homepage"
# Use a different config file
npx backstop test --config=backstop.staging.jsonMultiple config files are common when you test different environments (staging vs production) with different id values to keep baselines separate.
Common First-Run Problems
Screenshots are blank or show a loading spinner. Increase delay. Some apps need 1000–2000ms for JavaScript to finish rendering. Alternatively, use waitForSelector in your scenario to wait for a specific element instead of a fixed delay.
Font rendering differs between machines. This is the single most common source of false positives. The fix is Docker mode — run BackstopJS inside a container where font rendering is deterministic. See the Docker workflow post for details.
--no-sandbox is required in the engineOptions. This is normal when running inside Linux environments (CI, Docker, WSL). Chromium's sandbox requires kernel features that are not always available.
Test fails with Error: Navigation timeout. Your URL is slow or unreachable. Check the URL and add "readyEvent": null if the page never fires a ready event.
Structuring Your Scenario List
A useful starting set of scenarios for a typical web app:
- Public pages: homepage, about, pricing, blog index
- Auth pages: login, signup, password reset
- Authenticated views: dashboard, settings, profile (requires onReadyScript for login)
- Component states: empty state, error state, loading state
- Responsive breakpoints that are known to be tricky
Keep scenario labels consistent across your team. Labels appear in the HTML report and are used by --filter, so vague names like "Page 1" make debugging harder.
Next Steps
Once you have a working baseline with a dozen scenarios, the natural next step is plugging BackstopJS into your CI pipeline so every pull request is checked automatically. That requires Docker mode for rendering consistency — a fixed-font, fixed-resolution container that produces identical screenshots regardless of which CI runner picks up the job.
Visual regression testing with BackstopJS catches layout and style regressions early, but it does not replace functional testing. Tools like HelpMeTest complement the stack by covering user flows — form submissions, navigation, API interactions — in plain English scenarios that run alongside your visual checks. Together, they give you confidence that the app looks right and works right before every deploy.