Responsive visual testing: choosing viewports
How many viewports a visual suite actually needs, which widths catch the most breakage, why testing at your breakpoints is the wrong instinct, and how to keep the matrix from multiplying your bill.
Viewports multiply everything: your capture count, your CI minutes, your bill, and the number of diffs a human has to look at. Getting the list right is one of the highest-leverage decisions in a visual suite, and most teams get it wrong in the same specific way.
Test between your breakpoints, not at them
The instinct is to capture at 640, 768, 1024, 1280 — the exact values in your Tailwind config or media queries. That is precisely where the layout is best defined and least likely to surprise you.
Breakage lives in the middle of a range, where content has grown to fill space the designer never checked. A three-column grid at 1024px is fine. At 1100px, with a long product name, the third column wraps and pushes a button off its row. Nobody designed 1100px; it just happens to be a common laptop width.
A three-viewport default
| Width | Height | Represents | Why this one |
|---|---|---|---|
| 390 | 844 | Modern phone | The most common real-world mobile width band |
| 820 | 1180 | Tablet / small laptop | Between common breakpoints, where wrapping starts |
| 1440 | 900 | Desktop | The width most of your team is designing at |
Three viewports catch the overwhelming majority of responsive breakage. Adding a fourth roughly triples your cost relative to the marginal bugs it finds, which is a bad trade almost everywhere.
When to add a fourth
- 320px if you support older or budget devices — the tightest width where text overflow and horizontal scroll appear.
- 1920px if your product has large data tables or dashboards that stretch badly.
- A specific width your analytics show is unusually common for your audience.
That last one is the only principled way to extend the list. Pull the top viewport widths from your analytics and check whether your three cover them. Guessing produces a matrix that costs money and catches nothing.
What responsive testing catches that other tests do not
- Horizontal scroll on mobile. An element 10px too wide makes the whole page scroll sideways. No functional test notices; every user does.
- Collapsed or overflowing grids where a column count change was not anticipated for the content length.
- Touch targets that overlap once elements stack at narrow widths.
- Text that wraps into a new line and pushes content below the fold or out of a fixed-height container.
- Navigation that breaks between mobile and desktop modes — the range where neither the hamburger nor the full menu fits properly.
Horizontal scroll is worth a dedicated assertion, because it is cheap to check and almost always a genuine bug:
// Cheaper and more reliable than eyeballing a mobile screenshot.
test("no horizontal overflow at mobile width", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/pricing");
const overflows = await page.evaluate(() => {
const de = document.documentElement;
return de.scrollWidth > de.clientWidth;
});
expect(overflows).toBe(false);
});Keeping the matrix affordable
Routes × viewports × browsers × runs is the formula that decides your bill. Viewports are usually the cheapest dimension to cut because the failure modes correlate strongly — a layout broken at 820px is frequently broken at 768px too.
- Mobile and desktop on every pull request. Two viewports, because these are where most traffic and most breakage live.
- The full set nightly on the default branch. Tablet-width regressions are real but rarely urgent.
- All viewports on routes that changed structurally — a layout or grid change justifies the full sweep immediately.
The same reasoning applies to browsers, covered in cross browser visual testing tools, and the pipeline shape in visual testing in CI/CD pipelines.
Two determinism traps specific to viewports
Scrollbars. A page 2px taller than the viewport gains a scrollbar, which narrows the content area and shifts every centred element — producing a full-width diff from a two-pixel cause. Hide scrollbars in the test environment.
Device scale factor. Capturing at deviceScaleFactor: 2 produces images at twice the pixel dimensions. Pin it explicitly rather than inheriting the machine's, or baselines recorded on a retina laptop will not match a CI runner. More on both in how to fix flaky visual tests. Native apps are a different problem again — see mobile app visual testing.
Frequently asked questions
Which viewports should visual tests cover?
Three is the right default for most products: roughly 390×844 for mobile, 820×1180 for tablet or small laptop, and 1440×900 for desktop. Add a fourth only when analytics show a common width your three do not cover, or when you support very narrow devices at 320px.
Should I test at my CSS breakpoints?
No — test slightly above them. Layouts are best defined exactly at a breakpoint because that is where the rules change cleanly. Breakage happens in the middle of a range, where content has grown to fill space nobody designed for, so capture at 800px rather than 768px.
How do I detect horizontal scroll in a visual test?
Compare document.documentElement.scrollWidth against clientWidth at a mobile viewport. If scrollWidth is larger, something overflows. To find the cause, look for elements whose bounding rect extends past the viewport width and that are not inside an overflow-x: auto container.