Getting Started with BackstopJS: Visual Regression Testing for Web Apps

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 backstopjs

For the Playwright engine (optional, needed for Firefox/WebKit):

npm install --save-dev backstopjs
npx playwright install

Verify the install:

npx backstop --version

Initializing a Project

Inside your project root, run:

npx backstop init

This creates two things:

  • backstop.json — your configuration file
  • backstop_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 invalidated
  • viewports — each scenario is tested at every viewport; two viewports means two screenshots per scenario
  • misMatchThreshold — percentage of pixels allowed to differ before the test fails; 0.1 means 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 reference

This 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 test

BackstopJS 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 openReport

The 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 approve

This 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.json

Multiple 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.

Read more

Start now free