ScreenshotNeo

BlogHow-to

How to Automate Responsive Website Screenshots with Playwright Projects

Run one Playwright screenshot suite across browsers and mobile viewports. Configure projects, save images or compare visual baselines, and troubleshoot common failures.

By the ScreenshotNeo team4 October 20269 min read

Use Playwright Test projects to run the same tests with different browsers, device profiles, and viewport sizes. Use page.screenshot() to save an image, or toHaveScreenshot() to compare a page against a visual baseline. A project matrix checks responsive behavior under emulated browser settings; it does not prove the page behaves identically on a physical phone.

This guide configures a reusable responsive screenshot suite, shows how to run it, explains baseline updates, and covers the common sources of confusing screenshot results. The examples use TypeScript and Playwright Test. See the official projects guide, emulation guide, Page API, and visual comparisons guide for current details.

1. Install Playwright Test

In an existing Node.js project, install the test runner and its browser binaries:

npm init playwright@latest

Follow the setup prompts to create a configuration and example test if needed. If Playwright Test is already installed, install the browsers for that version:

npx playwright install

Keep the Playwright package and browser binaries aligned in local development and CI. Browser versions affect rendering, so changing the installed version can change screenshots even when your application code has not changed.

2. Define a responsive project matrix

Projects are named configurations that can run the same test files with different settings. Put stable viewport coverage in playwright.config.ts, so the matrix is visible and repeatable:

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

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'http://127.0.0.1:3000',
    trace: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium-desktop',
      use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } },
    },
    {
      name: 'chromium-mobile',
      use: { ...devices['iPhone 13'], viewport: { width: 390, height: 844 } },
    },
    {
      name: 'firefox-desktop',
      use: { ...devices['Desktop Firefox'], viewport: { width: 1440, height: 900 } },
    },
    {
      name: 'webkit-mobile',
      use: { ...devices['iPhone 13'], defaultBrowserType: 'webkit', viewport: { width: 390, height: 844 } },
    },
  ],
});

Device presets bundle settings such as browser identity, screen and viewport dimensions, and touch behavior. They can also be combined with context emulation for locale, timezone, geolocation, permissions, and color scheme. They represent emulated conditions, not the hardware and operating system of a real phone. For an explicit breakpoint check, set the exact viewport your layout needs. When spreading a device preset, put viewport after the spread so your dimensions override the preset.

Choose a matrix that answers a product question. A compact starting point might cover one desktop browser at a wide viewport and one mobile profile at a narrow viewport. Add Firefox, WebKit, or additional widths when you need that coverage. Each project adds work, so a matrix that combines many browsers, devices, and viewport sizes can lengthen runs. There is no universal device-coverage percentage that makes a matrix complete.

Use an explicit viewport without a device preset

If you care about a breakpoint rather than a branded device profile, use a viewport directly:

projects: [
  {
    name: 'tablet-breakpoint',
    use: {
      browserName: 'chromium',
      viewport: { width: 768, height: 1024 },
      isMobile: false,
    },
  },
],

For a one-off scenario within a test, change the viewport at runtime with page.setViewportSize(). Prefer project configuration when the viewport is a stable part of your test matrix; runtime changes are useful when a single scenario intentionally exercises multiple sizes.

3. Capture screenshots or assert visual baselines

Use a regular screenshot when you need an image file. Use a visual assertion when you need the test runner to compare the current rendering with a checked-in reference.

Save a viewport or full-page image

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

test('save the responsive page screenshot', async ({ page }) => {
  await page.goto('/');
  await page.screenshot({ path: 'artifacts/home-viewport.png' });
});

test('save the full document screenshot', async ({ page }) => {
  await page.goto('/');
  await page.screenshot({ path: 'artifacts/home-full.png', fullPage: true });
});

A viewport screenshot shows the visible area at the configured size. fullPage: true captures the full scrollable document. Full-page capture answers a different question: it can reveal problems below the fold, but it is not a substitute for checking the initial viewport at a breakpoint.

Screenshot output can use scale: 'css' for one image pixel per CSS pixel, or scale: 'device' for one image pixel per device pixel. Device scale can produce larger images on high-DPI profiles. Choose deliberately if image dimensions or file size matter.

Compare against a visual baseline

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

test('home page matches its visual baseline', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    animations: 'disabled',
  });
});

The first run creates a reference screenshot; later runs compare against it. With multiple projects, Playwright can include the project name in snapshot paths so each configuration has its own baseline. This matters when different engines or device settings render the same page differently.

Review changed images before accepting a new baseline. A changed screenshot can indicate an intended design update, a browser or environment change, or a genuine regression. Do not update snapshots automatically just to make a failing run green.

4. Run the matrix

Run all configured projects:

npx playwright test

Run one project while iterating:

npx playwright test --project=chromium-mobile

To create or update visual snapshots intentionally, use the runner’s update option after reviewing the code and expected rendering:

npx playwright test --update-snapshots

Commit approved baseline changes with the application change that explains them. Use the same command and project selection in CI as locally when you want comparable results.

5. Make screenshot comparisons repeatable

Visual comparisons are sensitive to the rendering environment. Playwright notes that rendering can vary with the host operating system, browser version, settings, hardware, power source, headless mode, and other factors. Generate and compare baselines in a consistent environment; for CI, use the same operating system image and Playwright version over time where practical.

  • Stabilize dynamic content. Freeze clocks or test data where appropriate, and mask timestamps, rotating promotions, avatars, or other genuinely variable regions.
  • Wait for the page to be ready. Navigate to the relevant state and wait for a meaningful locator or application signal before capturing. A fixed delay can help with a known animation or delayed content, but it is less reliable than waiting for the expected state.
  • Control animations. Disable animations for visual assertions when motion is not what the test covers. Keep a separate behavior test if animation itself matters.
  • Use comparison tolerances carefully. Options such as maxDiffPixels can allow small rendering differences. Set a tolerance based on acceptable variation, not to conceal a meaningful layout shift.
  • Filter irrelevant styling only when justified. Screenshot assertion options support stylesheet filtering. Use it to remove known volatile presentation, not to hide a styling regression the test should catch.
  • Separate viewport and full-page checks. Keep the screenshot scope explicit so a long page does not obscure a defect in the first screen, or vice versa.

6. Select coverage without making the suite unwieldy

Goal Useful configuration Tradeoff
Check a key mobile breakpoint One mobile emulation project with the target width and height Fast feedback, limited browser coverage
Check a wide desktop layout One desktop project at the design’s desktop width Does not cover narrow navigation or touch behavior
Catch engine-specific rendering differences Repeat representative viewports in Chromium, Firefox, and WebKit projects More project combinations and runtime
Inspect a long page Use full-page capture for selected pages Large images and more content that can vary
Validate actual hardware behavior Run a separate real-device test workflow Not established by ordinary Playwright device emulation

Start with the combinations tied to real layout breakpoints and supported browsers. Add a project when it covers a distinct risk, such as a menu that changes to a touch interaction or a layout that changes at a specific width. Running every possible combination increases time and snapshot maintenance without necessarily answering a new question.

7. Troubleshooting

Symptom Likely cause Fix
Project name is not recognized The name passed to --project does not exactly match the config Copy the project’s name value or run all projects with npx playwright test.
Browser executable is missing The installed Playwright package does not have its browser binaries installed Run npx playwright install for the package version in use.
Mobile screenshot has unexpected dimensions A device preset’s viewport or scale setting is being used, or an override was placed before the preset spread Put the explicit viewport after ...devices[...]; check whether output scale should be CSS or device pixels.
Snapshots differ on another machine Operating system, browser version, hardware, headless mode, or other rendering conditions differ Generate and compare baselines in a consistent environment and inspect the diff before updating.
Screenshot catches a loading state The test captures before the relevant page content is ready Wait for a locator or application-ready signal before capture. Avoid arbitrary long sleeps where a condition can be awaited.
Snapshot changes on every run Dynamic content, animation, or time-dependent output is visible Use deterministic fixtures, disable irrelevant animation, or mask only the volatile region.
Full-page capture misses lazy content Content is loaded only after scrolling or another user action Trigger the intended loading behavior before capture, or test the relevant region separately. A full-page option does not prove every app-specific lazy-loading path works.
Update command hides an unexpected difference Baselines were regenerated without reviewing the visual change Inspect the actual and expected images, then update only when the design change is intended.

8. Performance, reliability, and cost

Playwright project coverage consumes your own CI or local compute; this workflow has no per-screenshot service charge. Runtime grows as you add projects and tests, although the exact time depends on your suite and environment. Keep the matrix focused, use a single project for quick feedback, and reserve broader browser coverage for the checks that need it.

For reliable baselines, pin dependencies, install matching browsers, and use consistent rendering environments. Keep screenshots as CI artifacts when they help diagnose failures, and review changes to snapshot files as code changes. Visual assertions are a regression signal, not proof that every responsive state or physical-device behavior is correct.

Or skip the browser setup

If you need a clean capture without maintaining a browser matrix, ScreenshotNeo is a website screenshot API and MCP server. The API takes one GET request and returns an image or PDF. 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,
)
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 accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.

Sign up for ScreenshotNeo free: 1,000 screenshots a month, no card required.

FAQ

Can I change viewport size inside a test?

Yes. Use page.setViewportSize() when a single test needs to exercise multiple widths. Use project configuration for stable matrix entries.

Does a mobile project test a real iPhone or Android phone?

No. Device profiles emulate browser and context properties. Physical hardware testing is a separate workflow; Playwright documents a distinct Android API that requires a device or Android Virtual Device and is experimental.

Should every page use a full-page visual baseline?

No. Use full-page comparisons for pages where below-the-fold layout matters. Keep viewport checks for first-screen responsive behavior and use focused assertions for dynamic or very long pages.

Can I use projects just to export screenshots?

Yes. A test can call page.screenshot() and save an artifact without using visual assertions. Use toHaveScreenshot() when you want a baseline comparison.