Residential Proxies Web Scraping API Turn Sites Into AI Data Automate 3000+ Apps Learn Python Automation Pay As You Go Proxies
Residential Proxies Web Scraping API
Pay As You Go Proxies 10 Free Proxies Antidetect Browser No Code Browser Bots Web Data For AI Agents Hire Scraper Builders

How to Set Up BackstopJS for Visual Regression Testing

Updated August 2026
BackstopJS is the most established open source visual regression testing tool, using headless Chrome to capture screenshots, run pixel comparison against approved baselines, and generate detailed HTML reports showing exactly what changed. It works standalone with a JSON configuration file, requires no test framework, and runs well in Docker for consistent CI rendering. This guide walks through setup, configuration, scenario scripting, and CI integration.

BackstopJS fills a specific niche in the visual testing landscape: teams that want screenshot comparison without adopting a full test framework or paying for a cloud service. If you already use Playwright or Cypress for E2E testing, their built-in or plugin-based visual testing is likely a better fit because it integrates into your existing tests. BackstopJS is strongest when you want to visually test a set of URLs, like a marketing site, a documentation portal, or a multi-page application, without writing test code.

Install BackstopJS

Install BackstopJS as a project dependency or globally:

# As a project dependency (recommended)
npm install --save-dev backstopjs

# Or globally
npm install -g backstopjs

Initialize the project to create the configuration file and directory structure:

npx backstop init

This creates backstop.json (the configuration file), a backstop_data directory (where baselines and test results are stored), and example engine scripts. The default configuration includes sample scenarios you can replace with your own URLs.

Configure Scenarios and Viewports

Edit backstop.json to define what you want to test. The two key sections are viewports (the screen sizes to test at) and scenarios (the pages to capture):

{
  "id": "my-project",
  "viewports": [
    { "label": "phone", "width": 375, "height": 812 },
    { "label": "tablet", "width": 768, "height": 1024 },
    { "label": "desktop", "width": 1440, "height": 900 }
  ],
  "scenarios": [
    {
      "label": "Homepage",
      "url": "https://your-site.com",
      "delay": 1000,
      "misMatchThreshold": 0.1
    },
    {
      "label": "Pricing",
      "url": "https://your-site.com/pricing",
      "delay": 1000,
      "misMatchThreshold": 0.1
    },
    {
      "label": "Contact",
      "url": "https://your-site.com/contact",
      "delay": 500,
      "misMatchThreshold": 0.1
    }
  ],
  "engine": "puppeteer",
  "report": ["browser"],
  "paths": {
    "bitmaps_reference": "backstop_data/bitmaps_reference",
    "bitmaps_test": "backstop_data/bitmaps_test",
    "html_report": "backstop_data/html_report"
  }
}

Each scenario generates one screenshot per viewport, so three viewports and three scenarios produce nine screenshots total. The delay field sets how many milliseconds to wait after page load before capturing, giving fonts, images, and dynamic content time to render. The misMatchThreshold is the percentage of pixel difference allowed before a scenario fails.

Additional scenario options let you scroll to specific elements, capture only a portion of the page, or hide elements before capture:

{
  "label": "Blog Post",
  "url": "https://your-site.com/blog/example-post",
  "selectors": [".article-content"],
  "hideSelectors": [".cookie-banner", ".live-chat-widget"],
  "removeSelectors": [".sidebar-ad"],
  "delay": 1500,
  "misMatchThreshold": 0.2
}

The selectors field limits the screenshot to specific DOM elements. hideSelectors sets elements to visibility: hidden (preserving layout), while removeSelectors sets elements to display: none (collapsing their space).

Create Reference Baselines

Capture the initial baseline screenshots:

npx backstop reference

BackstopJS opens each scenario URL in headless Chrome at each configured viewport, waits for the specified delay, and saves the screenshots to backstop_data/bitmaps_reference/. Review the captured images to verify they represent the correct appearance. These are now your approved baselines.

Commit the baselines to version control if you want them versioned alongside code. Some teams commit baselines, others generate them in CI from a known-good deployment. Committing them gives you traceability (you can see when and why baselines changed), while generating them in CI avoids large binary files in your repository.

Run Tests and Review Results

After code changes, run the test comparison:

npx backstop test

BackstopJS captures fresh screenshots and compares each one against the stored baseline. If any scenario exceeds its misMatchThreshold, the test fails. In both cases, BackstopJS generates an HTML report and opens it in your browser.

The report shows each scenario with three images: the reference (baseline), the test (current capture), and the diff (highlighting changed pixels in magenta). Passing scenarios show a green status, and failing scenarios show red with the specific percentage of pixels that differ. The side-by-side view makes it immediately obvious what changed, whether it is a shifted element, a color difference, or a missing component.

When changes are intentional, approve them by running:

npx backstop approve

This copies the current test screenshots to the reference directory, making them the new baselines. You can also approve individual scenarios by passing the scenario label.

Add Interaction Scripts

Some pages need interaction before the interesting visual state is visible: a dropdown needs to be opened, a tab needs to be clicked, a form needs to be filled. BackstopJS supports Puppeteer scripts that run before screenshot capture.

Create a script file, for example backstop_data/engine_scripts/clickTab.js:

module.exports = async (page, scenario) => {
  await page.waitForSelector('.tab-pricing');
  await page.click('.tab-pricing');
  await page.waitForTimeout(500);
};

Reference it in the scenario:

{
  "label": "Pricing Tab Active",
  "url": "https://your-site.com/pricing",
  "onReadyScript": "clickTab.js",
  "delay": 500,
  "misMatchThreshold": 0.1
}

The onReadyScript runs after the page loads and before the screenshot is captured. You can also use onBeforeScript to set cookies, localStorage, or authentication state before the page loads:

module.exports = async (page, scenario) => {
  await page.setCookie({
    name: 'auth_token',
    value: 'test-token-value',
    domain: 'your-site.com',
  });
};

This handles pages behind authentication without maintaining a separate login flow in your visual tests.

Run in Docker for CI Consistency

The most common source of BackstopJS false positives is cross-platform rendering differences. The same page renders differently on macOS and Linux because of font rendering, anti-aliasing, and available system fonts. Docker eliminates this by providing an identical rendering environment everywhere.

BackstopJS supports Docker natively. Add the Docker flag to your configuration:

{
  "dockerCommandTemplate": "docker run --rm -i --mount type=bind,source=\"{cwd}\",target=/src backstopjs/backstopjs:{version} {backstopCommand} {args}"
}

Now backstop reference and backstop test run inside the official BackstopJS Docker container automatically. Generate baselines in Docker, run tests in Docker, and the screenshots match regardless of the host operating system.

In CI, run the Docker container directly:

# GitHub Actions example
jobs:
  visual-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx backstop test --docker
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: backstop-report
          path: backstop_data/html_report/

The HTML report artifact gives reviewers a complete visual diff report they can open in any browser without running tests locally.

Advanced Configuration and Troubleshooting

Beyond the basic setup, BackstopJS has configuration options that solve common problems teams encounter as their test suites grow.

Full-page capture with scrolling. By default, BackstopJS captures only the visible viewport area. For pages longer than the viewport height, set "fullPage": true in the scenario to capture the entire scrollable page. This is useful for landing pages and documentation pages where content extends well below the fold. Be aware that full-page screenshots produce larger image files and increase comparison time, so use this selectively on pages where below-the-fold content matters.

The readyEvent option. The delay setting works for most pages, but it is a blunt tool: you are guessing how long the page needs. For single-page applications that load data asynchronously, use "readyEvent": "backstopjs_ready" instead. Then add document.dispatchEvent(new Event('backstopjs_ready')) in your application code after the dynamic content finishes rendering. BackstopJS waits for this event instead of relying on a fixed timer, which is both more reliable and faster because it does not wait longer than necessary.

The readySelector option. If you cannot modify the application source to dispatch an event, use "readySelector": ".data-loaded" to tell BackstopJS to wait until a specific element appears in the DOM before capturing. This works well for pages where a loading spinner is replaced by content, because you point the selector at the content container rather than guessing a delay value.

Handling cookie banners and modals. Cookie consent banners and promotional modals cause constant false positives because they appear unpredictably or with slightly different timing. Use "removeSelectors" to strip them from the DOM before capture. If a modal needs to be dismissed first (because it covers content you want to test), use an onReadyScript that clicks the dismiss button and waits for the animation to complete before capture proceeds.

Large test suites. BackstopJS runs scenarios sequentially by default, which gets slow with dozens of URLs. Set "asyncCaptureLimit": 5 in the top-level config to run up to 5 scenarios in parallel. Increase this value on machines with more CPU and memory, but avoid setting it too high because headless Chrome instances consume significant resources and can cause out-of-memory failures or rendering glitches when overloaded. A value between 3 and 10 works well for most CI environments.

Filtering test runs. When working on a specific page, running the entire suite is wasteful. Use the --filter flag to run only matching scenarios: npx backstop test --filter="Homepage". The filter matches against scenario labels, so naming your scenarios consistently (by page name or feature area) makes filtering practical.

When BackstopJS Is the Right Choice

BackstopJS works best for teams that want visual testing with minimal complexity. A JSON config file, a few CLI commands, and Docker for consistency is the entire setup. There is no test framework to learn, no service to authenticate, and no subscription to manage.

It is especially effective for content-heavy sites like documentation portals, marketing sites, and blogs where the pages are mostly static and the visual test scenarios are "load this URL and capture it." For interactive applications where scenarios require complex user flows before capture, the interaction scripts work but are more limited than what Playwright or Cypress test code can express.

The tradeoffs compared to framework-integrated solutions: no built-in review dashboard (you review HTML reports), pixel-only comparison without AI filtering, and separate tooling from your functional tests. For teams that want unified functional and visual testing in one framework, Playwright's built-in screenshots or Cypress with plugins provide tighter integration.

Key Takeaway

BackstopJS gives you effective visual regression testing with just a JSON config file and three commands: reference, test, and approve. Run it in Docker for cross-platform consistency, and you have a zero-cost visual testing pipeline that catches UI regressions on every deployment.