ScreenshotNeo

BlogHow-to

How to test responsive breakpoints with BackstopJS

Test responsive layouts at your project’s actual CSS breakpoints with BackstopJS. Configure viewport coverage, create references, review diffs, and keep captures stable.

By the ScreenshotNeo team4 October 20269 min read

To test responsive breakpoints with BackstopJS, add viewport sizes just below and above the CSS widths where your layout changes, capture approved reference screenshots, and run backstop test to compare future renders. BackstopJS runs the sizes you configure; it does not discover your CSS breakpoints automatically. Choose widths from your own styles and test data, not from generic phone, tablet, and desktop labels alone.

This guide uses BackstopJS configuration in JavaScript. It covers setup, breakpoint selection, reference management, capture scope, stable rendering, failure diagnosis, and CI considerations. The core commands and options are documented in the BackstopJS project and its npm documentation; check the documentation for the version installed in your project when details are version-sensitive.

1. Choose viewport widths around your real breakpoints

Start with the CSS that controls the layout. If a navigation bar changes at 48rem, determine the effective pixel width for the browser environment you use, then include widths on both sides of that transition. Also include the transition width itself when the exact boundary matters. Repeat this for other meaningful layout changes, such as a grid changing column count or a sidebar moving below the main content.

For example, if your project has a transition near 768 CSS pixels, a useful set might include 767, 768, and 769 pixels. Those values are an example, not a universal breakpoint recommendation. Add widths where the layout is especially sensitive, and avoid adding many nearly identical widths unless they reveal a distinct risk. The viewport height also matters: choose a representative height that makes the relevant content visible, and keep it consistent across runs.

What to cover How to select it Why it helps
Breakpoint boundary Widths immediately below, at, and above a project transition Reveals off-by-one and boundary behavior
Stable layout regions One or more widths within each important layout mode Checks the layout away from the transition too
Known sensitive width A width where wrapping, overflow, or navigation has caused problems Targets a project-specific risk

BackstopJS requires at least one viewport. Its root viewports array applies the configured dimensions to your scenarios; a scenario can also define its own viewports to override the root list. See the documented viewport and scenario configuration.

2. Install BackstopJS and create a configuration

In a project with Node.js and npm available, install BackstopJS as a development dependency:

npm install --save-dev backstopjs

Create backstop.config.js at the project root. Replace the sample URL, viewport sizes, and readiness selector with values that match your application. The selectors below are examples: use an element that appears when the page has reached the state you want to compare.

module.exports = {
  id: 'responsive-breakpoints',
  viewports: [
    { label: 'nav-below-768', width: 767, height: 900 },
    { label: 'nav-at-768', width: 768, height: 900 },
    { label: 'nav-above-768', width: 769, height: 900 },
    { label: 'wide-layout', width: 1280, height: 900 }
  ],
  scenarios: [
    {
      label: 'home-page',
      url: 'http://localhost:3000/',
      readySelector: '[data-test="page-ready"]',
      selectors: ['document'],
      misMatchThreshold: 0.1,
      requireSameDimensions: true
    }
  ],
  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'],
  engine: 'playwright',
  asyncCaptureLimit: 5,
  asyncCompareLimit: 50
};

The selectors: ['document'] setting captures the full document. If you want to focus only on what is visible in the viewport, use selectors: ['viewport']. To inspect a component, use a CSS selector such as ['.site-header']. BackstopJS also supports capturing selected DOM elements; choose the smallest scope that still shows the failure, or use separate scenarios when a component view and a page view answer different questions. Capture scope is described in the BackstopJS documentation.

BackstopJS’s configuration and engine options can vary by version. If your installed version uses a different engine setup or config format, follow its matching project documentation. Keep the config in version control so the tested widths and capture rules are reviewable.

3. Capture references, run tests, and review changes

  1. Start the application in the same state you intend to test, including any needed test data or authentication.
  2. Capture the initial reference set: npx backstop reference --config=backstop.config.js. These images become the visual baseline.
  3. Run the comparison: npx backstop test --config=backstop.config.js. BackstopJS captures the current page at the configured viewports and compares the new images with the references.
  4. Inspect the report and diffs. Check which scenario and viewport failed, then determine whether the difference is an intended design change, unstable content, an environment difference, or a regression.
  5. Approve only intentional changes: npx backstop approve --config=backstop.config.js. This promotes the latest changed captures into the references. Do not use approval as a shortcut for clearing an unexplained failure.

These commands are the normal reference, test, and approval workflow documented by BackstopJS. Use meaningful scenario and viewport labels: they make failures easier to identify in reports. You can narrow a rerun with --filter to matching scenario labels; for example:

npx backstop test --config=backstop.config.js --filter=home-page

4. Configure scenarios for the states that matter

A scenario represents a URL and page state. Add separate scenarios for routes or states whose content, interactions, or layout differs. For example, test a product page separately from the home page, and test a menu-open state separately from a menu-closed state. A shared viewport list is applied across scenarios unless a scenario overrides it.

Useful scenario settings include:

Setting Purpose Guidance
label, url Name the case and identify the page state Use labels that identify route and state; these are required scenario fields.
viewports Override global dimensions for one scenario Use only when a scenario needs different width coverage.
selectors Capture the document, viewport, or chosen elements Pick a scope that makes the breakpoint defect visible.
readySelector Wait for a chosen selector to appear Prefer an explicit page-ready element for asynchronous pages.
readyEvent Wait for a named application console event Use when the app can signal readiness explicitly.
delay Wait a fixed number of milliseconds Use as a fallback when there is no better readiness signal; fixed delays can be fragile.
hideSelectors, removeSelectors Hide or remove unstable regions from a capture Do not hide a region whose size or responsive behavior is under test.
misMatchThreshold Set tolerated pixel difference The documented default is 0.1; inspect actual diffs before changing it.
requireSameDimensions Fail when compared image dimensions differ The documented default is true; treat this separately from pixel tolerance.

The options and defaults above are documented by the BackstopJS npm documentation. The mismatch threshold is the documented default, not a guarantee that a particular value is right for every project. A permissive threshold can hide small layout defects; a strict threshold can expose harmless rendering variation. Review representative diffs before adjusting it.

5. Keep screenshots deterministic

Visual comparison works best when the same test state produces the same page. Use stable test data or static stubs for dynamic content where practical. Wait for a specific readiness condition instead of choosing an arbitrary long delay. If a loading spinner, rotating banner, timestamp, or third-party widget changes between runs, consider whether it belongs in the test. BackstopJS documents readiness options such as readySelector, readyEvent, and delay, along with selector hiding or removal for unstable elements.

Be careful with hidden content. Hiding an animated advertisement may reduce irrelevant diffs, but hiding the header or navigation under breakpoint test defeats the purpose. When a screenshot is blank or incomplete, first check whether the URL loaded and whether the readiness condition points to an element that actually appears in that state.

Text can render differently across operating systems and browser environments. The BackstopJS project recommends Docker rendering to reduce environment-related variation. Docker helps keep the rendering environment more consistent; it does not make every application render identically in every dependency or configuration.

6. Common failures and fixes

Symptom Likely cause What to check or change
Every run reports broad visual differences Reference and test environments differ, or dynamic page content changes Use the same app build, test data, browser environment, and readiness rule. Inspect the diff before changing thresholds.
Screenshot is blank or missing content Wrong URL, app not running, route failure, or capture occurs before content is ready Open the URL in the test environment, check app logs, then use an appropriate readySelector or readyEvent.
Only one viewport fails A breakpoint-specific layout issue, viewport label/config error, or a real boundary defect Inspect that viewport’s diff and verify the configured width and height. Rerun the matching scenario with --filter.
Text or antialiasing differences appear across machines Different rendering environments Keep runs in a consistent environment; consider Docker as documented by the project.
Failure is caused by changing content Live or time-dependent data differs between captures Use deterministic test data where possible. Hide or remove only regions that are irrelevant to the test.
Image dimensions changed Content or capture dimensions differ; same-dimension enforcement is enabled Determine whether the height change is a regression. Change requireSameDimensions only when dimension changes are expected and do not represent a defect.
Small visible defect passes Mismatch tolerance is too permissive Review the pixel diff and lower misMatchThreshold carefully; confirm the setting behaves as expected in your installed version.
Repeatedly flaky test Unstable state, timing, or external dependency Make data and app state deterministic, use an explicit ready signal, and avoid relying on an arbitrary delay alone.

7. Runtime, reliability, and maintenance

Capture work grows with the number of scenario and viewport combinations: each configured viewport is tested for each applicable scenario. More widths improve coverage only when they represent a distinct transition or risk, and they increase the amount of capture and comparison work. Start with meaningful boundaries, then add widths in response to actual layout risks or defects.

Keep the same viewport dimensions, application state, test data, and rendering environment between reference creation and regression runs. When a test fails, preserve the old references until you understand the change. Review and approve intentional design updates in the same change context as the code that caused them, so reviewers can see why the baseline changed.

Keep pixel tolerance and dimension enforcement as separate decisions. misMatchThreshold controls tolerated pixel difference; requireSameDimensions controls whether image dimension changes fail. Neither setting selects breakpoints for you. There is no universal set of widths or threshold supported by the project documentation; choose based on your CSS, failure history, and diff review.

Or skip the browser setup

If you need screenshots of URLs without configuring and maintaining a browser capture flow, ScreenshotNeo provides a screenshot API and MCP server. For responsive testing, you can request a viewport size, save the response as an image, and compare captures in your own workflow. This is a screenshot capture option; BackstopJS remains the tool in this guide that manages visual references and comparisons.

See the ScreenshotNeo API documentation for request options. A direct request looks like this:

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 removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed, and response headers indicate the page verdict and billing status. Its MCP server offers screenshot tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. View the docs and sign up for 1,000 free screenshots a month, with no card.

FAQ

Does BackstopJS find my CSS media queries automatically?

No. You choose viewport dimensions based on the breakpoints and layout risks in your project.

Should I use one scenario per viewport?

Usually, a shared viewport list is simpler when the same page state should be tested at every width. Use scenario-specific viewports when a case needs different coverage.

When should I approve new references?

After confirming that the visual difference is expected and correct. Approval changes the baseline future tests use.

Which capture scope should I use?

Use document for full-page coverage, viewport for the visible area, and a CSS selector to isolate a component. Choose based on where the responsive defect could appear.

Can I compare different viewport sizes in one run?

Yes. Put the sizes in the root viewports array and run the reference and test commands. Each configured viewport is captured for the applicable scenarios.