How to Make Website Screenshot Tests Stable When the Page Resizes
Stabilize responsive screenshot tests by setting the viewport deliberately, controlling page state, and matching the environment used for your baselines.
To make website screenshot tests stable when a page resizes, choose the viewport deliberately, set it before navigation when testing an initial layout, wait for the relevant application state, and compare screenshots in a consistent browser and operating-system environment. With Playwright, use toHaveScreenshot(): it waits for two consecutive screenshots to match before comparing the result with the expected image. That helps with rendering that is still settling, but it does not freeze changing application data or guarantee every page will become stable.
1. Define what the test should prove
Decide whether the test checks an initial responsive layout or a live resize transition. These are different behaviors and are easier to diagnose as separate tests.
- Initial layout: set the target viewport before navigation, then load the page at that size.
- Resize transition: navigate at the starting size, resize deliberately, and assert the resulting layout.
- Device behavior: use device emulation when user agent, screen size, or touch behavior matters in addition to viewport dimensions.
Choose explicit sizes based on the responsive behavior your product promises to support. Include representative narrow and wide layouts and sizes around the breakpoints that matter to the application. There is no universal required set of widths.
2. Set the viewport before navigation
For an initial-layout test, configure the viewport before calling goto(). Playwright advises this because many websites do not expect a phone to change size after loading. Its setViewportSize() API also resets the screen size. If screen and viewport need independent values, configure screen and viewport in the browser context instead.
3. Complete Playwright example
The following example creates two explicit viewport projects and tests the initial page layout at each size. Save it as tests/responsive.spec.ts in a Playwright Test project. Adjust the URL and viewport values to match your app’s supported layouts.
import { test, expect } from '@playwright/test';
test('home page renders at the responsive viewport', async ({ page }) => {
// The project config sets the viewport before this navigation.
await page.goto('http://127.0.0.1:3000/');
// Wait for the application state that matters to this screenshot.
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
// The assertion waits for two consecutive matching screenshots.
await expect(page).toHaveScreenshot('home-page.png', {
fullPage: false,
animations: 'disabled',
});
});
Configure separate projects in playwright.config.ts so each viewport has its own expected screenshot:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'narrow-chromium',
use: { browserName: 'chromium', viewport: { width: 390, height: 844 } },
},
{
name: 'wide-chromium',
use: { browserName: 'chromium', viewport: { width: 1440, height: 900 } },
},
],
});
Install and run with the commands for your Playwright project:
npm install --save-dev @playwright/test
npx playwright install chromium
npx playwright test tests/responsive.spec.ts
If testing the transition itself, use a separate test and resize after navigation:
test('navigation adapts after a live resize', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('http://127.0.0.1:3000/');
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole('button', { name: 'Open menu' })).toBeVisible();
await expect(page).toHaveScreenshot('home-page-after-resize.png', {
animations: 'disabled',
});
});
4. Control screenshot options deliberately
| Choice | Use it when | Stability consideration |
|---|---|---|
viewport |
Testing a defined browser layout | Keep width and height identical for baseline and comparison. |
| Device emulation | User agent, screen size, or touch behavior affects the feature | Keep the device configuration consistent. Do not assume viewport width alone emulates a device. |
fullPage |
The whole scrollable page is part of the assertion | Page height and lazy-loaded content can change the capture. Use viewport-only capture when below-the-fold content is outside the test. |
animations: 'disabled' |
Motion is not being tested | Playwright disables animations for the assertion; finite animations are fast-forwarded and infinite animations are canceled for capture, then resumed. |
mask or screenshot styles |
A timestamp or unrelated rotating widget adds noise | Limit masks to volatile areas outside the test’s purpose. Do not mask responsive layout or content whose movement matters. |
maxDiffPixels and comparison thresholds |
A known, small rendering variance must be tolerated | Keep tolerance narrow and review diffs. A permissive threshold can hide a real regression. |
| Screenshot scale | Comparing across standard or high-DPI output | Keep scale consistent. CSS-pixel scale gives one image pixel per CSS pixel; device-pixel scale can produce larger images on high-DPI devices. |
Use an explicit wait for the page state relevant to the assertion, such as a visible heading or completed data state. The consecutive-screenshot check helps detect settling output, but it does not stop a backend response, clock, random value, or live feed from changing between renders. Make test data deterministic where possible.
5. Keep the rendering environment consistent
Generate baselines and comparisons using the same browser version, operating system, browser settings, hardware conditions, and headless mode where practical. Playwright documents these as factors that can change rendering. If the project intentionally tests multiple browsers or operating systems, keep their screenshot expectations associated with those configurations instead of comparing their pixel output as interchangeable.
When an assertion fails, inspect the diff before updating expected images. Use npx playwright test --update-snapshots only after confirming the visual change is intentional. Automatically refreshing baselines can silence the very regression the test should catch.
6. Troubleshooting unstable screenshots
| Symptom | Likely cause | Fix |
|---|---|---|
| Layout differs between the baseline and test | Viewport was set at a different time or has different dimensions | For initial layout tests, set the project viewport before navigation and keep dimensions identical. Test live resizing separately. |
| Text wraps or elements shift on another machine | Browser, OS, fonts, settings, hardware, or headless mode differ | Create and compare baselines in a consistent environment; use distinct project baselines for intentional environment coverage. |
| Only animated areas fail | Motion is still running or has a changing frame | Use animations: 'disabled' when animation is not under test. For animation behavior, test it separately with an explicit state or timing strategy. |
| Timestamp, promotion, or chat area changes | Dynamic content is unrelated to the layout assertion | Control the data or narrowly mask/restyle that region. Leave the responsive elements under test visible. |
| Full-page screenshot has different dimensions | Content height, lazy loading, or page state changed | Use viewport capture if page length is irrelevant. If full-page behavior matters, make content and loading state deterministic before asserting. |
| Many tiny diffs remain | Scale or rendering environment is inconsistent, or a tolerance is too strict for known noise | First align environment and scale. Only then consider a small documented tolerance; do not broadly raise pixel thresholds. |
| Large layout changes pass unexpectedly | Mask covers important content or difference tolerance is too permissive | Remove broad masks and reduce tolerance; inspect the actual expected and received images. |
7. Performance, reliability, and cost
Every additional viewport project adds browser work and screenshot comparisons, so prioritize layouts that cover promised responsive behavior and meaningful breakpoints. Reuse the same deterministic test data and environment to make failures easier to reproduce. Avoid full-page captures when the test only concerns the visible viewport, since they capture more content and introduce page-height and lazy-loading variables.
Screenshot tests have no universal pixel threshold or viewport count. The right values depend on the behavior under test. Keep the viewport, screenshot scale, browser configuration, and application state explicit. This improves reliability while keeping the test suite focused.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request captures a URL as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options, including viewport and device settings.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan. Learn more at ScreenshotNeo.
Sign up for 1,000 free screenshots a month with no card.
FAQ
Should I set the viewport before or after navigation?
Before navigation when asserting the initial layout at a target size. Resize after navigation only when the resize transition is the behavior under test.
Does waiting for two matching screenshots make the page deterministic?
No. It waits for consecutive screenshot output to match, but changing application data or external content can continue changing.
Should I update snapshots whenever a test fails?
No. Review the visual diff and update only when the change is intentional.
Do all viewport sizes need the same baseline?
No. Each intended viewport or device configuration should have expectations that correspond to that configuration.


