ScreenshotNeo

BlogHow-to

How to Capture the Same Web Page at Multiple Mobile Device Sizes Automatically

Use Playwright to capture one URL across repeatable mobile profiles and viewport sizes, save clearly named screenshots, and keep visual comparisons consistent.

By the ScreenshotNeo team4 October 202611 min read

Use Playwright to open the same URL in a separate browser context for each mobile device profile or viewport size, then save one screenshot per context. Configure the viewport before navigation, wait for the page state your team needs, and put the profile or dimensions in each filename. A viewport alone does not emulate every mobile behavior: device profiles can also set the user agent, screen size, device scale factor, mobile behavior, and touch support.

This guide uses Playwright with JavaScript. It covers device profiles, custom sizes, viewport and full-page captures, repeatable visual checks, common failures, and a hosted API option. See the official Playwright emulation documentation and Page API for the settings and methods used below.

1. Install Playwright

For a small standalone capture script, install Playwright and its Chromium browser. Run these commands in a new project directory:

npm init -y
npm install -D playwright
npx playwright install chromium

Save the following as capture.mjs. It uses built-in Playwright device profiles, creates a fresh context for each profile, and saves one viewport screenshot per device. The example is runnable as written; change targetUrl to the page you want to capture.

2. Capture one URL across mobile device profiles

import { chromium, devices } from 'playwright';
import { mkdir } from 'node:fs/promises';

const targetUrl = 'https://example.com';
const outputDir = 'screenshots';
const deviceNames = ['iPhone 13', 'Pixel 7'];

await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });

try {
  for (const deviceName of deviceNames) {
    const profile = devices[deviceName];
    if (!profile) throw new Error(`Unknown Playwright device: ${deviceName}`);

    // Each context gets the profile's viewport, user agent, scale factor,
    // touch support, and mobile settings.
    const context = await browser.newContext({ ...profile });
    const page = await context.newPage();

    try {
      await page.goto(targetUrl, {
        waitUntil: 'domcontentloaded',
        timeout: 45_000,
      });
      // Replace or supplement this with an app-specific ready condition.
      await page.locator('body').waitFor({ state: 'visible' });

      const safeName = deviceName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
      await page.screenshot({
        path: `${outputDir}/${safeName}.png`,
        fullPage: false,
        animations: 'disabled',
      });
      console.log(`Saved ${outputDir}/${safeName}.png`);
    } finally {
      await context.close();
    }
  }
} finally {
  await browser.close();
}

Run it with:

node capture.mjs

Playwright’s device registry supplies profiles such as iPhone 13 and Pixel 7. Profile availability depends on the installed Playwright version; inspect the installed registry if a name is unknown. Device emulation is useful for repeatable responsive checks, but it does not establish that a page behaves exactly as it would on every physical phone.

3. Capture custom mobile dimensions

Use explicit viewport dimensions when the review targets breakpoints or design-system widths rather than named devices. Set the viewport on the context before opening the page so the site sees the intended size during its initial load. This example captures a matrix of width and height pairs:

import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

const targetUrl = 'https://example.com';
const sizes = [
  { name: 'small-phone', width: 320, height: 700 },
  { name: 'phone', width: 390, height: 844 },
  { name: 'large-phone', width: 430, height: 932 },
];

await mkdir('screenshots', { recursive: true });
const browser = await chromium.launch({ headless: true });

try {
  for (const size of sizes) {
    const context = await browser.newContext({
      viewport: { width: size.width, height: size.height },
      deviceScaleFactor: 1,
      isMobile: true,
      hasTouch: true,
    });
    const page = await context.newPage();

    try {
      await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
      await page.locator('body').waitFor({ state: 'visible' });
      await page.screenshot({
        path: `screenshots/${size.name}-${size.width}x${size.height}.png`,
        fullPage: false,
        animations: 'disabled',
      });
    } finally {
      await context.close();
    }
  }
} finally {
  await browser.close();
}

Dimensions are CSS pixels. deviceScaleFactor controls the relationship between CSS pixels and screenshot pixels; use a profile or set the scale factor deliberately if you need retina-density output. When you use isMobile, hasTouch, or a user agent, choose values that match the behavior you are trying to examine. A width-only matrix is appropriate for checking breakpoints, but may miss touch-specific or mobile-layout behavior.

4. Choose viewport or full-page capture

A viewport screenshot records only the visible screen at the current scroll position. A full-page screenshot captures the scrollable page in one image. Choose based on the review question:

  • Viewport: navigation, first-screen layout, sticky elements, and what a person initially sees.
  • Full page: long-form layout, section order, footer placement, and page-wide visual review.

To switch the profile example to full-page output, set fullPage: true. Be aware that extremely long pages can produce large images and that lazy-loaded content may not exist until it is scrolled into view. If the page loads images or sections on scroll, scroll through it or use an application-specific preparation step before capturing. A full-page image is not the same as a sequence of screenshots taken while a real phone scrolls.

await page.screenshot({
  path: 'screenshots/full-page.png',
  fullPage: true,
  animations: 'disabled',
});

5. Wait for the page state you need

domcontentloaded means the initial document has been parsed; it does not mean every client-rendered section, image, or third-party widget is ready. Use a selector that represents usable page content when the page has an application-specific ready state:

await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.locator('[data-testid="page-ready"]').waitFor({
  state: 'visible',
  timeout: 15_000,
});

If there is no ready marker, wait for a meaningful heading or content selector, or use a short bounded delay for a known animation or delayed render. Avoid waiting indefinitely for all network activity: analytics, polling, and chat connections can keep requests open. For an animation-heavy page, Playwright’s screenshot option animations: 'disabled' can help reduce motion-related differences. Keep any wait strategy the same across runs and baselines.

6. Make captures repeatable

For a useful comparison, keep the capture environment and inputs stable:

  • Use the same Playwright version, browser build, operating system or CI image, and headless setting when generating and reviewing baselines.
  • Keep the URL, authentication state, locale, time zone, color scheme, viewport, device scale factor, and test data consistent.
  • Wait for the same application-ready condition on every size.
  • Use unique, descriptive filenames that include the device or dimensions; avoid overwriting unrelated captures.
  • Disable or stabilize animated content, rotating banners, timestamps, and randomized data where possible.

Playwright documents that screenshot output can vary with the operating system, browser settings, hardware, headless mode, and other environment factors. Use the same environment where the baseline was created. For automated visual regression, Playwright Test provides screenshot assertions; see Visual comparisons. Do not compare screenshots captured in different environments as if every pixel difference necessarily represents a product regression.

7. Use Playwright Test for visual regression

When the goal is to detect changes over time, put each size in a named test and compare with an approved baseline. A minimal test file can look like this:

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

const targetUrl = 'https://example.com';
const sizes = [
  { name: 'small-phone', width: 320, height: 700 },
  { name: 'phone', width: 390, height: 844 },
  { name: 'large-phone', width: 430, height: 932 },
];

for (const size of sizes) {
  test(`page at ${size.name}`, async ({ browser }) => {
    const context = await browser.newContext({
      viewport: { width: size.width, height: size.height },
      isMobile: true,
      hasTouch: true,
    });
    const page = await context.newPage();

    try {
      await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
      await page.locator('body').waitFor({ state: 'visible' });
      await expect(page).toHaveScreenshot(`${size.name}.png`, {
        fullPage: false,
        animations: 'disabled',
      });
    } finally {
      await context.close();
    }
  });
}

Install the test runner with npm install -D @playwright/test and run with npx playwright test. The first approved run creates baseline snapshots; subsequent runs compare against them. Review and update a baseline only after confirming the visual change is intended. Use the same browser and environment for baseline creation and comparison.

8. Hosted responsive testing options

For interactive review, BrowserStack Responsive Testing documents preset and custom resolutions, side-by-side responsive simulation, and screenshot capture for an individual device or all displayed devices. For responsive visual regression, Percy supports snapshots at multiple widths and responsive DOM snapshots. These hosted workflows can reduce local setup, but check the provider’s current usage accounting and plan terms before relying on a particular number of widths. Percy documents that each responsive width counts as a separate screenshot against monthly screenshot usage. See BrowserStack Responsive Testing, Percy responsive testing, and responsive DOM snapshots.

Choose between local automation, an interactive hosted session, and a visual regression service based on whether you need scripted repeatability, hands-on comparison, or managed snapshot review. Emulation helps inspect responsive layouts; use physical-device testing when the question depends on device-specific browser behavior or hardware.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a PNG, JPEG, WebP, or PDF. To capture the same page at multiple viewport sizes, send one request per size and record the dimensions in your output names. The API accepts the parameter names other screenshot APIs use, which can make switching easier. See the ScreenshotNeo API documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d width=390 \
  -d height=844 \
  -o stripe-390x844.webp

Repeat the request with the other dimensions and distinct output filenames. For example, change width and height to 320 and 700, then to 430 and 932.

Python

import requests

url = "https://stripe.com"
sizes = [(320, 700), (390, 844), (430, 932)]

for width, height in sizes:
    response = requests.get(
        "https://api.screenshotneo.com/v1/shot",
        params={
            "access_key": "YOUR_API_KEY",
            "url": url,
            "width": width,
            "height": height,
        },
        timeout=90,
    )
    response.raise_for_status()
    filename = f"stripe-{width}x{height}.webp"
    with open(filename, "wb") as image_file:
        image_file.write(response.content)
    print(f"Saved {filename}")

Node.js

const url = 'https://stripe.com';
const sizes = [[320, 700], [390, 844], [430, 932]];

for (const [width, height] of sizes) {
  const q = new URLSearchParams({
    access_key: 'YOUR_API_KEY',
    url,
    width: String(width),
    height: String(height),
  });
  const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
  const image = new Uint8Array(await res.arrayBuffer());
  const { writeFile } = await import('node:fs/promises');
  const filename = `stripe-${width}x${height}.webp`;
  await writeFile(filename, image);
  console.log(`Saved ${filename}`);
}

The endpoint returns the requested capture as binary data. Check the HTTP response before saving it as an image, and keep the API key on a trusted server or local development environment rather than exposing it in public client-side code. The same capture can also use full-page output, dark mode, device presets, custom CSS or JavaScript, wait conditions, caching, and other documented options; consult the docs for the supported parameter names and values.

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

10. Troubleshooting

Symptom Likely cause Fix
Unknown device profile The profile name is absent from the installed Playwright version or spelled differently. Check devices from the installed package, use a supported profile name, or configure the viewport and emulation fields directly.
Page looks like desktop at a mobile width Only the viewport was changed, or the page’s responsive behavior depends on mobile settings or its viewport meta tag. Use a device profile or explicitly configure the user agent, isMobile, and touch support as appropriate. Confirm the page declares a responsive viewport.
Screenshot is blank or missing content Capture happened before client-side rendering completed, navigation failed, or a page element is hidden until interaction. Check navigation errors and status, wait for a meaningful ready selector, and perform required interactions before capture.
Some images are missing Images may be lazy-loaded below the fold or delayed by network activity. For a full-page capture, scroll through the page to trigger lazy loading and wait for relevant image elements to load before capturing.
Run hangs or times out The site is slow, a navigation event never completes, or persistent requests prevent a network-idle condition. Use a bounded timeout and a narrower readiness condition such as domcontentloaded plus a page selector. Avoid unbounded waits.
Snapshots differ between runs Browser or operating system changed, fonts or data are unstable, or animation and dynamic content are active. Pin the runtime, use stable test data and fonts, disable animations, and remove or mask dynamic regions using the visual test setup.
Screenshot dimensions do not match expected pixels CSS viewport size and device scale factor are being confused. Set both explicitly. A viewport is measured in CSS pixels; device scale factor changes raster pixel density.
Full-page image is unexpectedly tall or expensive to review The document is long or has content that expands during scrolling. Use viewport captures for first-screen checks, or capture selected page sections when a full document image is not needed.
Hosted visual checks consume more snapshots than expected Responsive widths may be counted separately by the provider. Review the current provider usage rules and limit the width matrix to the breakpoints that answer the review question.

11. Performance, reliability, and cost

Each profile or viewport requires a page navigation and screenshot, so total run time grows with the number of sizes and the site’s load time. Start with widths that cover your actual breakpoints, reuse the launched browser, and create a separate context per configuration. Add concurrency only after considering the target server’s rate limits and whether simultaneous runs can produce different page state. A context per size also prevents cookies, local storage, and page state from leaking between captures.

For reliable jobs, give navigation and readiness waits explicit timeouts, log the URL and profile for each failure, and retry only transient failures. A retry can still produce a different result if the page content changes. Keep output names deterministic and preserve the environment metadata alongside visual baselines. Local Playwright has no per-screenshot hosted usage charge described in the cited documentation, but it does require a machine, browser installation, and CI or developer time. Hosted tools may account for each responsive width separately; verify current terms directly before estimating recurring cost.

12. FAQ

Should I use device presets or custom dimensions?

Use presets when user agent, touch, and mobile emulation matter together. Use custom dimensions for breakpoint coverage or a specific viewport. For important mobile behavior, test both responsive widths and a real device.

Does a mobile screenshot prove the page works on a phone?

No. Browser emulation covers configurable browser and device characteristics, but it does not reproduce every physical device, operating system, browser, or hardware condition.

How many sizes should I capture?

Choose the smallest set that covers the breakpoints and device behaviors relevant to the page. Include the narrowest supported width, widths around layout transitions, and any sizes required by your visual review.

Can I capture several sizes in parallel?

Yes, if the machine and target site can handle concurrent navigations. Start sequentially for predictable results, then add bounded concurrency if runtime matters and the site remains stable under load.