ScreenshotNeo

BlogHow-to

How to Test Responsive Web Pages with Playwright Screenshots

Build reliable responsive screenshot tests with Playwright: choose meaningful viewport states, control rendering differences, and review visual changes safely.

By the ScreenshotNeo team4 October 202610 min read

Use Playwright Test’s toHaveScreenshot() assertion to compare responsive page screenshots against checked-in reference images. Choose viewport widths around your application’s actual CSS layout transitions, make the rendering environment consistent, and inspect every baseline update. Device presets can add emulated settings such as touch and user agent, but they do not guarantee identical behavior to every physical device.

This guide uses TypeScript with @playwright/test. It covers viewport selection, runnable tests, configuration, screenshot scale, comparison tolerances, CI stability, troubleshooting, and an API option for captures outside your test suite.

1. Choose viewport widths from your layout

Do not treat a list of popular device names as responsive coverage. First inspect the application’s CSS breakpoints and identify the layout states they create: for example, navigation collapse, columns stacking, content width changing, or a dialog becoming full-screen. Test the transitions just below and above each important breakpoint, plus representative widths within each resulting state.

For a breakpoint at 768 CSS pixels, a useful starting point is to test at 767 and 768 (or 769 if that better represents the next state), then include a typical narrow mobile and desktop width. These are examples, not universal recommendations: derive the values from the app’s CSS and supported layouts.

What you need to catch Useful screenshot setup
Layout changes at a CSS breakpoint Explicit viewport sizes just below and above the breakpoint
Representative composition within a layout state One or more widths that reflect actual supported content conditions
Touch or user-agent dependent behavior A Playwright device profile, with any necessary viewport overrides
Content below the visible viewport A separately named full-page screenshot

Viewport width and browser/platform coverage are separate decisions. A viewport tests responsive layout at that width; a browser or operating system matrix tests rendering differences. Add projects for browsers and platforms your product supports, and expect those environments may need distinct baselines.

2. Install Playwright Test and configure projects

Install the test runner if it is not already in the project:

npm init playwright@latest

For an existing project, install @playwright/test using the project’s package manager and install the browsers required by your setup. Keep Playwright and browser versions consistent between baseline creation and CI comparison.

Here is an illustrative playwright.config.ts using explicit viewports and a device profile:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'http://127.0.0.1:3000',
    // Keep visual comparison in one stable browser environment.
    browserName: 'chromium',
  },
  projects: [
    {
      name: 'mobile-narrow',
      use: { viewport: { width: 390, height: 844 } },
    },
    {
      name: 'breakpoint-below',
      use: { viewport: { width: 767, height: 900 } },
    },
    {
      name: 'breakpoint-at',
      use: { viewport: { width: 768, height: 900 } },
    },
    {
      name: 'desktop-wide',
      use: { viewport: { width: 1280, height: 800 } },
    },
    {
      name: 'iphone-emulation',
      use: { ...devices['iPhone 13'] },
    },
  ],
});

Replace the sample widths, profile, browser, and base URL with values that match the application and its supported browsers. An explicit viewport is a compact way to test a layout width. A profile can also configure properties such as screen size, user agent, and touch. It is an emulation preset, not proof that every physical device behaves identically. The Desktop Chrome profile, for example, supplies a Windows-specific user agent; unset its user agent if you want the host platform’s user agent instead.

Playwright’s documented defaults include a 1280×720 context viewport and a device scale factor of 1, but relying on defaults makes the intended coverage less obvious. Set the conditions your baseline depends on explicitly. See the official Playwright emulation guide and BrowserType API.

3. Write screenshot assertions for responsive states

toHaveScreenshot() captures repeatedly until two consecutive screenshots match, then saves the reference on its first run. Later runs compare against that reference. Use descriptive, distinct names so a failed comparison identifies the affected state.

import { test, expect } from '@playwright/test';

test('landing page at a narrow mobile viewport', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/');
  await expect(page).toHaveScreenshot('landing-mobile-390.png');
});

test('landing page around the navigation breakpoint', async ({ page }) => {
  await page.setViewportSize({ width: 767, height: 900 });
  await page.goto('/');
  await expect(page).toHaveScreenshot('landing-breakpoint-below.png');
});

test('landing page above the navigation breakpoint', async ({ page }) => {
  await page.setViewportSize({ width: 768, height: 900 });
  await page.goto('/');
  await expect(page).toHaveScreenshot('landing-breakpoint-at.png');
});

Save the test, then run the project with npx playwright test. On the first run, Playwright creates reference screenshots; review and commit the intended references. Subsequent runs compare new captures to them. Configure a local web server for the app in playwright.config.ts when needed, so local and CI runs start the same app command and wait for the same URL.

Make the page state repeatable before taking the screenshot. Use stable test data, predictable application state, and loaded fonts. If content changes on every run—such as timestamps, rotating banners, random avatars, or remote data—control that input in the test environment or hide only the known dynamic region. Avoid masking a large area, since it can conceal the regression the screenshot is meant to catch.

4. Decide between viewport and full-page captures

A default page screenshot represents the current viewport and is best for checking responsive composition: what is visible, how navigation fits, and whether columns or controls overlap. To include content below the fold, pass fullPage: true in the screenshot assertion options:

test('article page full-page layout at mobile width', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/article/example');
  await expect(page).toHaveScreenshot('article-mobile-full.png', {
    fullPage: true,
  });
});

Keep full-page and viewport assertions separately named. A full-page image answers whether the entire rendered page is visually as expected; a viewport image answers how a user sees a specific responsive state. Lazy-loaded content and sticky elements can make full-page captures behave differently, so verify that the page has loaded the content the assertion is intended to cover. The Page API documents screenshot options including fullPage and screenshot scale.

5. Keep screenshot rendering consistent

Playwright cautions that rendering can vary with the host OS, browser version, settings, hardware, power source, headless mode, and other factors. Generate and compare a baseline in the same environment where possible. In CI, pin the environment and browser version used for screenshots; align fonts, browser settings, and screenshot configuration too. If you intentionally compare multiple platforms, treat each platform’s rendering as its own test environment and baseline set.

Other practical sources of variation include network-loaded assets, animation, caret blinking, time-dependent content, and asynchronous page updates. Prefer local fixtures or controlled responses for data the screenshot relies on. Wait for a meaningful page condition rather than adding an arbitrary long delay:

test('account page after its main content is ready', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/account');
  await page.getByRole('heading', { name: 'Account' }).waitFor();
  await expect(page).toHaveScreenshot('account-mobile.png');
});

Only wait for a selector that genuinely represents the page state being tested. A heading becoming visible does not necessarily mean every image or font has finished loading; add application-specific readiness checks where those resources matter.

6. Choose CSS-pixel or device-pixel scale

The screenshot scale option accepts css or device. CSS scale produces one image pixel per CSS pixel. Device scale produces one image pixel per device pixel, so a high-DPI capture can be larger. A context’s deviceScaleFactor also affects the emulated device; its documented default is 1.

For layout-oriented comparison, CSS scale often keeps image dimensions easier to inspect. Choose device scale when high-DPI appearance itself is part of the requirement. Keep the choice and the context’s device scale factor consistent when creating and checking a given baseline set.

test('mobile layout at CSS-pixel screenshot scale', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/');
  await expect(page).toHaveScreenshot('home-mobile-css.png', {
    scale: 'css',
  });
});

See Playwright’s Page screenshot API and BrowserType context options for the current option details.

7. Set comparison tolerances and review updates

Start with a strict visual comparison in a stable environment. If a known, unavoidable rendering difference remains, Playwright provides options such as maxDiffPixels and shared snapshot configuration. There is no universal safe threshold: a higher tolerance can hide a real layout regression. Document why a tolerance exists and inspect the diff when it triggers.

test('product page with a narrow reviewed tolerance', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/products/example');
  await expect(page).toHaveScreenshot('product-mobile.png', {
    maxDiffPixels: 20,
  });
});

The value above is only an example, not a recommended threshold. Determine any tolerance from observed, reviewed noise in your own stable environment.

When an intentional UI change causes a failure, regenerate references with npx playwright test --update-snapshots. Review the new screenshot and the diff, then commit the approved baseline with the UI change. Do not update references just to silence a failure; a baseline represents expected appearance. Playwright documents both comparison options and snapshot updates in its visual comparisons guide.

8. Run responsive visual tests in CI

  1. Use a consistent CI image, Playwright version, browser version, and font set for baseline creation and routine comparison.
  2. Run the same projects and screenshot options in CI as locally; avoid generating a baseline on one platform and comparing it on another unless that difference is intentional.
  3. Upload the actual screenshot and diff artifacts when a comparison fails, so reviewers can see whether the change is expected.
  4. Review and commit snapshot changes with the corresponding UI change, and keep the viewport names meaningful.
  5. Keep the matrix targeted: test the breakpoints where layout changes and representative widths for important states, then add a browser or platform when support requirements call for it.

Visual tests add capture and comparison work for each page and project. A broad matrix can increase CI time and the number of baselines reviewers maintain. Prioritize states that can reveal distinct layout failures instead of multiplying near-identical widths.

9. Troubleshooting common failures

Symptom Likely cause What to check or change
Screenshot differs on every run Dynamic content, animation, late-loading assets, or inconsistent rendering environment Stabilize test data and page readiness; align OS, browser version, fonts, settings, and headless mode. Inspect actual and diff images before changing tolerance.
Many screenshots fail only in CI CI and local machine render differently, or browser/font versions differ Generate and compare baselines in the same pinned CI environment; check installed fonts and browser version.
Mobile screenshot looks like desktop The test did not set the intended viewport, or a profile’s viewport was overridden Set an explicit viewport in the project or test and inspect the effective context settings. Verify the test is running under the expected project.
Text or images are missing in the capture The assertion began before the relevant application content or resource was ready Wait for an application-specific selector or readiness condition; use controlled assets where appropriate.
Full-page screenshot is unexpectedly tall or incomplete Lazy content has not loaded, or the page’s scroll-dependent behavior changes capture results Trigger the application’s intended content-loading behavior and confirm which below-the-fold content should be present; keep a viewport assertion for above-the-fold layout.
A small visual change is ignored Comparison tolerance is too permissive Review maxDiffPixels and shared snapshot settings; reduce tolerance only after inspecting the rendered noise.
Snapshot update removes a real regression Reference was updated without reviewing the diff Restore or inspect the old reference, review actual and expected images, and update only for an approved UI change.
Device profile does not match a physical handset Emulation sets browser parameters but does not reproduce every physical-device behavior Use emulation for repeatable coverage, then check on the actual target device if the finding depends on hardware, OS integration, or real browser behavior.

10. Or skip the browser setup

If you need a screenshot for review or an external workflow rather than a Playwright assertion, ScreenshotNeo returns a screenshot from one GET request. It is a website screenshot API and MCP server for developers. The API accepts common screenshot parameter names used by other screenshot APIs, which can make switching easier. See the ScreenshotNeo website and API documentation.

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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report 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. These API captures are useful for obtaining images, but they do not replace Playwright’s baseline assertions in a responsive visual regression suite.

Sign up free for 1,000 screenshots a month with no card.

Frequently asked questions

Does Playwright test every real phone with a device preset?

No. A preset emulates browser and device parameters. It does not establish identical behavior across all physical devices. Use real target hardware when a result depends on hardware or OS behavior.

Should every CSS breakpoint have a screenshot test?

Cover breakpoints that create meaningful layout changes, especially widths just below and above them. Add representative widths for important states based on the app’s supported use cases.

Should I use screenshot tests instead of responsive unit tests?

They answer different questions. A screenshot catches rendered visual changes; focused unit or component tests can verify specific behavior and state. Use screenshots for visual expectations that are difficult to express as assertions on DOM structure alone.

Can I reuse one baseline across browsers and operating systems?

Only when the rendering environments are intentionally equivalent for the comparison. Browser and platform differences can affect pixels, so separate projects and baselines may be appropriate.