How to Run Screenshot Tests for a Website on Multiple Viewport Sizes in Playwright
Run Playwright visual tests at representative viewport sizes with separate projects, stable baselines, and actionable failure reports.
Use Playwright Test projects to run the same visual test at several viewport sizes. Give each project a name, configure its viewport, and use await expect(page).toHaveScreenshot() to create and compare a baseline for that project. Keep baseline creation and comparison runs on the same browser and operating system so rendering differences do not create misleading failures.
This guide covers viewport-only checks, device emulation, project configuration, snapshot review, stability, troubleshooting, and CI considerations. It uses illustrative viewport dimensions; choose sizes that exercise your site’s actual responsive breakpoints.
1. Install Playwright Test
In an existing Node.js project, add Playwright Test and install the browser binaries:
npm install --save-dev @playwright/test
npx playwright install
The examples use TypeScript syntax, which Playwright Test supports directly in its test and configuration files. If your project already has Playwright configured, keep its existing installation and browser setup.
2. Configure a project for each viewport
Projects let the same test run with different browser or device configurations. Add a project per representative layout size in playwright.config.ts. A project name identifies the run and can be included in its screenshot baseline path.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'desktop-1280',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1280, height: 800 },
},
},
{
name: 'tablet-768',
use: {
...devices['Desktop Chrome'],
viewport: { width: 768, height: 1024 },
},
},
{
name: 'mobile-390',
use: {
...devices['Desktop Chrome'],
viewport: { width: 390, height: 844 },
},
},
],
expect: {
toHaveScreenshot: {
pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
},
},
});
The sizes above are examples, not an official device matrix. Cover the widths where your layout changes and any important supported page shapes. Three well-chosen widths can expose more useful issues than a large set of arbitrary sizes, while a broad matrix increases runtime and snapshot maintenance.
When using a device descriptor, spreading it provides its device configuration and setting viewport afterward overrides its default viewport dimensions. Use a descriptor when you need the profile’s user agent, screen size, or touch behavior as well as its viewport. A viewport-only project checks responsive CSS at those dimensions; it does not establish that every real phone, operating system, or browser has been tested. See Playwright’s project guide and emulation guide.
3. Write one screenshot test and run it across projects
Put the test in tests/homepage.spec.ts. The same test file runs once in each configured project:
import { test, expect } from '@playwright/test';
test('homepage visual layout', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
For page.goto('/'), set baseURL in the Playwright use configuration or navigate to an absolute URL. For example, add baseURL: 'http://127.0.0.1:3000' to the shared use settings and start the application before running the test. A complete local setup can use Playwright’s webServer configuration to start the app for the test run; see the web server configuration.
Run all configured projects or select one to iterate faster:
npx playwright test
npx playwright test --project=mobile-390
By default, the first run without a reference image creates a baseline. Later runs compare their rendered screenshot with it. Playwright’s screenshot assertion waits for two consecutive screenshots to match before comparing the final image with the expectation. These assertions use the Playwright Test runner. See the visual comparisons guide and screenshot assertion API.
4. Choose viewport sizes that answer a real question
A useful responsive suite represents the site’s supported layouts and likely failure points. Include widths near CSS breakpoints, where navigation, columns, or spacing change; a common desktop layout; and a narrow layout where content wrapping or overflow is likely. Heights matter too when the visible fold or fixed elements are part of the design.
- Viewport-only coverage: Set
viewportin each project. This is appropriate for checking CSS media queries and layout at selected dimensions. - Device-profile coverage: Use a relevant
devicesdescriptor when touch behavior, user agent, and screen configuration matter. Override the viewport after spreading the descriptor when you need different dimensions. - Multiple browser engines: Add browser projects when cross-browser rendering matters. Each additional browser and viewport combination adds execution time and baselines to review.
Do not describe a viewport simulation as testing a physical device. Device emulation applies browser settings such as viewport, screen, user agent, and touch; it is not a guarantee of identical behavior on all hardware.
5. Keep baselines reviewable and comparisons stable
Commit accepted reference screenshots alongside the code. When a visual change is intentional, regenerate snapshots, inspect the resulting images, and commit only the reviewed updates:
npx playwright test --update-snapshots
The command updates expected images; it does not decide whether the change is correct. Review each affected viewport for unintended clipping, overflow, shifted content, and broken interactions before accepting it.
Rendering can vary with operating system, browser version and settings, hardware, power source, and headless mode. Generate and compare baselines in a consistent environment, especially in CI. A baseline created on one OS or browser build can differ from a comparison on another even when application code is unchanged. Playwright documents these sources of variation in its visual comparison guidance.
Project names in the path template keep viewport snapshots separate. This makes it clear which dimensions failed and avoids comparing one viewport against another’s golden image.
6. Control dynamic page state and tune comparison options
Before capturing, make the page state repeatable. Use deterministic test data, wait for the content the test actually needs, and remove or mask known volatile regions where they are irrelevant to the layout assertion. Playwright screenshot assertions support stylePath for applying a stylesheet during capture, along with tolerance settings such as threshold and maxDiffPixels.
stylePath: Hide or normalize a known dynamic region with a screenshot-only stylesheet. Keep the rule narrow so it does not hide the layout under test.threshold: Allows a chosen perceived color difference when comparing pixels. Change it only when a known harmless rendering variation requires it.maxDiffPixels: Allows a selected count of differing pixels. A higher allowance can conceal a real regression, so use the smallest justified value.- Animations and hover: Screenshot assertions disable animations by default. Hover state is still part of the captured appearance; move the pointer away if hover is not the intended state.
For example, a deliberately scoped screenshot assertion can be written as:
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 20,
// Add stylePath only when a narrow, known dynamic region must be normalized.
});
The number here is an example, not a general recommendation. Start with strict comparisons, identify the cause of a difference, then set the narrowest tolerance that addresses harmless variation. Consult the API reference for supported options in your installed Playwright version.
7. Resize a page directly when you need an in-test check
Projects are usually clearer when the same suite should run in a stable set of viewports. For a focused test that changes dimensions during one test, use page.setViewportSize() before navigation:
import { test, expect } from '@playwright/test';
test('layout changes at a narrow viewport', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
The Page API notes that setting the viewport also resets screen size. Many sites do not expect a phone-sized viewport to be applied after the page has already loaded, so set it before navigation. For richer control over viewport and screen, configure the browser context instead. See setViewportSize.
8. Run the suite in CI with predictable inputs
Use the same Playwright browser version and operating environment when creating and checking snapshots. Make the app’s startup command, base URL, test data, locale, and any external dependencies predictable. For each failed project, inspect the expected and actual images and the diff before changing the baseline or tolerance.
Keep the matrix proportional to risk: each extra viewport, browser engine, and test page adds captures and expected images to maintain. Start with representative breakpoints and expand coverage where a layout or browser difference has caused real defects. The research sources provide no universal runtime benchmark, so measure your own suite as it grows.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Every run reports screenshot differences | Baseline and comparison use different OS, browser version, settings, headless mode, or hardware. | Use a consistent environment and browser build for baseline generation and comparison; inspect diffs before updating snapshots. |
| A viewport uses the wrong baseline | Project snapshots are not separated by project name or the test uses an ambiguous screenshot path. | Include {projectName} in pathTemplate and use a stable screenshot name. |
| The first run creates snapshots unexpectedly | No reference image exists yet. | Review the generated baseline images and commit them as the initial expectation. Later runs will compare against them. |
| A test fails because the page has not loaded | The app server is not running, baseURL is missing or incorrect, or navigation is targeting the wrong URL. |
Start the app for the run, configure a correct baseURL, and verify the navigation target. Use Playwright’s webServer setting for managed startup. |
| Only dynamic portions differ | Timestamps, rotating content, random data, or third-party elements change between runs. | Stabilize test data or narrowly hide/normalize the volatile region with stylePath. Avoid broad masks and loose tolerances. |
| Mobile layout changes are missing | The test sets only a desktop viewport, or assumes viewport dimensions also emulate touch and user agent. | Add a mobile-sized project for responsive CSS; use an appropriate device descriptor when those additional settings matter. |
| Hover styling appears in the image | The pointer is over an interactive element at capture time. | Move the mouse to a neutral location before the assertion if hover is not the state being tested. |
| Updating snapshots hides a regression | New references were accepted without examining the changed pixels. | Inspect every updated image and diff. Keep the update only when it matches an intentional UI change. |
10. Performance, reliability, and maintenance
Test runtime grows with the number of project and test combinations. A focused viewport matrix reduces capture time and the volume of images reviewers must inspect. Use a smaller set for frequent pull request checks and add broader browser or device coverage where the supported experience calls for it.
Reliability depends on controlling inputs and rendering conditions: use a stable browser and host, deterministic page content, a known application state, and narrowly chosen comparison tolerances. Screenshot diffs are evidence to review, not an automatic judgment that a change is wrong. The research provides no official cross-project performance figures, so avoid treating any fixed viewport count as a speed guarantee.
Direct Playwright screenshots run in your project’s browser setup and produce repository-managed baselines. If you instead need a hosted screenshot of a page without maintaining browser capture setup, ScreenshotNeo provides a website screenshot API and MCP server. Its API captures a page image or PDF; it is not a substitute for Playwright’s baseline assertion and diff review workflow.
Or skip the browser setup
For an on-demand screenshot rather than a Playwright visual regression assertion, make one GET request. See the ScreenshotNeo API documentation for the request options.
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,
)
r.raise_for_status()
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', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed; 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. The same features are available on every plan. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Do I need one test file per viewport?
No. Configure multiple projects and let the same test file run under each project’s settings.
Does a mobile viewport test prove the site works on a real phone?
No. A viewport-only project checks rendering at those dimensions. Device profiles add simulated settings such as touch and user agent, but do not test every physical device.
Should I update snapshots whenever CI fails?
No. First determine whether the UI change was intentional or the environment or page state changed. Review the diff, then update and commit the baseline only for an accepted visual change.


