ScreenshotNeo

BlogHow-to

How to Test Responsive Web Pages with Puppeteer Screenshots

Test responsive layouts with Puppeteer by setting viewports before navigation, capturing the right page area, and comparing results at your site’s breakpoints.

By the ScreenshotNeo team4 October 20269 min read

To test responsive pages with Puppeteer, set an explicit viewport or device profile before navigating, load the page, wait for the content your test depends on, then capture a viewport, full page, clipped region, or element. Repeat at widths chosen around your own layout breakpoints and compare the screenshots. Puppeteer screenshots show how Chromium rendered those cases; they are not a substitute for testing on physical devices.

1. Set up Puppeteer

The examples below use Node.js and Puppeteer. Install Puppeteer in a project:

npm install puppeteer

Puppeteer downloads a compatible Chrome for Testing by default. If your environment supplies a browser separately, consult the official Puppeteer getting started guide for the corresponding installation and launch configuration.

2. Capture a page at several responsive widths

This runnable script accepts a URL, sets each viewport before navigation, captures the rendered viewport as a PNG, and closes the browser even if a capture fails. Replace the sample URL and widths with your own page and breakpoint cases.

// responsive-screenshots.mjs
import puppeteer from 'puppeteer';

const targetUrl = process.argv[2] ?? 'https://example.com';
const cases = [
  { name: 'narrow', width: 375, height: 812 },
  { name: 'medium', width: 768, height: 1024 },
  { name: 'wide', width: 1440, height: 900 },
];

const browser = await puppeteer.launch({ headless: true });
try {
  for (const testCase of cases) {
    const page = await browser.newPage();
    try {
      await page.setViewport({
        width: testCase.width,
        height: testCase.height,
        deviceScaleFactor: 1,
      });

      const response = await page.goto(targetUrl, {
        waitUntil: 'networkidle2',
        timeout: 60_000,
      });
      if (response && !response.ok()) {
        throw new Error(`Navigation returned HTTP ${response.status()}`);
      }

      await page.screenshot({ path: `responsive-${testCase.name}.png` });
      console.log(`Saved responsive-${testCase.name}.png`);
    } finally {
      await page.close();
    }
  }
} finally {
  await browser.close();
}

Run it with node responsive-screenshots.mjs https://your-site.example/page. networkidle2 is the wait condition used in Puppeteer’s screenshot guide example, not a universal definition of page readiness. Analytics, long polling, streaming requests, and other background activity can make network-idle waits unsuitable. For a page with a clear ready state, consider waitUntil: 'domcontentloaded' or 'load' and then wait for a selector that matters to the test:

await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="page-ready"]');

Choose the readiness condition based on what needs to appear in the screenshot. A page can finish navigation before its client-rendered content, fonts, or images are ready.

3. Choose viewport dimensions and device emulation

For layout checks, explicit width and height make cases easy to reproduce. Use widths around transitions defined by your CSS and design: include a value just below a breakpoint, one just above it, and widths where important content is close to wrapping or overflowing. There is no universal responsive test matrix; the useful cases depend on the site’s own breakpoints and content.

Puppeteer’s Page.setViewport() reference says to set the viewport before navigation, because many websites do not expect a phone-sized viewport to be applied after loading. It also notes that changes to isMobile or hasTouch can cause a page reload. Each page in a browser can have its own viewport.

Device emulation is useful when the test needs a known device’s metrics and user agent in addition to dimensions. Puppeteer provides Page.emulate() as a shortcut for setting a device’s user agent and viewport. Apply it before navigation:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const { KnownDevices } = puppeteer;
  await page.emulate(KnownDevices['iPhone 15']);
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.screenshot({ path: 'phone.png' });
} finally {
  await browser.close();
}

Use a device profile when user-agent-sensitive behavior is relevant. For ordinary CSS layout comparisons, explicit dimensions are usually simpler. Browser emulation does not prove how a physical device, operating system, or browser will render the page.

4. Capture the right area

Puppeteer supports viewport screenshots, full-page screenshots, rectangular clips, and element screenshots. See the official screenshots guide and ScreenshotOptions reference.

Viewport screenshot

page.screenshot() captures the currently visible page area by default. This is a good fit for checking navigation, columns, and above-the-fold layout at a specific viewport.

Full-page screenshot

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

Use fullPage: true to capture the full scrollable page. This can reveal content below the fold, though very long pages may produce large images and take longer to render and save.

Clipped region

await page.screenshot({
  path: 'header.png',
  clip: { x: 0, y: 0, width: 1200, height: 180 },
});

A clip is a rectangle in page coordinates. Keep it within the rendered page bounds and use it to focus comparisons on a particular region. Screenshot options also include captureBeyondViewport; consult the current API reference when combining it with clipping or other capture settings.

Element screenshot

const card = await page.waitForSelector('[data-testid="product-card"]');
if (!card) throw new Error('Product card was not found');
await card.screenshot({ path: 'product-card.png' });

An element screenshot is useful for comparing a component across widths without capturing the whole page. Puppeteer’s guide says element capture attempts to scroll the element into view if it is hidden. Ensure the target selector uniquely identifies the component under test.

5. Make captures comparable

  • Keep inputs stable. Use the same URL, account state, test data, viewport height, and capture scope across runs.
  • Wait for meaningful readiness. Wait for a page-specific selector or other known condition before capturing. For lazy-loaded images, scroll the relevant content into view and wait for the image to load if it is part of the check.
  • Account for animation. If motion makes captures differ between runs, disable animations in the test environment or use page CSS to set a consistent state before capture.
  • Record the case. Include width, height, whether a device profile was applied, and the capture scope in filenames or test output.
  • Inspect transitions. Compare cases around your site’s own breakpoints and areas where text wrapping, navigation, grids, or images may change.

For reproducible image output, select a format and settings deliberately. Puppeteer supports path, type, quality, fullPage, clip, captureBeyondViewport, and omitBackground. The documented default type is PNG; quality applies to JPEG and WebP, not PNG. If type is omitted, the filename extension can determine the format when a path is supplied. Check the current options reference for details and defaults.

await page.screenshot({
  path: 'responsive.webp',
  type: 'webp',
  quality: 85,
  fullPage: true,
  omitBackground: false,
});

Use PNG when you want lossless output for pixel-level inspection. JPEG and WebP can reduce file size when lossy output is acceptable. An omitted background is useful for transparent output where supported and appropriate to the page.

6. Resize the browser window when needed

For responsive CSS, setting the page viewport is generally the direct way to test dimensions. If your test specifically needs to resize the browser window, Puppeteer’s window management guide notes that inner-window sizing updates asynchronously. Wait for the resize event before capturing so the screenshot reflects the new size. Do not assume that calling a window-resize operation means the page has already finished responding.

7. Troubleshoot common problems

Symptom Likely cause Fix
Mobile screenshot still looks like desktop The viewport or device profile was applied after navigation, or the page did not receive a mobile viewport. Set the viewport or call page.emulate() before page.goto(). Check that the target page has responsive CSS and a viewport meta tag.
Navigation times out waiting for network idle The site keeps requests active, such as analytics or streaming connections. Use a different navigation condition such as domcontentloaded, then wait for a page-specific selector or readiness signal.
Screenshot is blank or content is missing Client rendering, fonts, images, or other asynchronous work has not completed. Wait for the relevant element and, where needed, verify image loading or application readiness before capture.
Some content is absent in a full-page capture Content may load lazily only after it approaches the viewport. Scroll through the page in steps and wait for the relevant content before capturing; make the loading strategy part of the test.
Element selector returns no handle The selector does not match, the component has not rendered, or it is in a different frame. Wait for a stable selector, confirm it exists on the page, and use the correct frame if the content is embedded.
Screenshot dimensions or format are unexpected The capture scope, output type, filename extension, or clip may not match the intended case. Set type and dimensions explicitly, and check whether the capture is viewport, full page, clip, or element-level.
One viewport case appears stale or different A responsive script may run on resize, or a page may retain state between cases. Create a fresh page per case, set its viewport before navigation, and wait for resize-driven work if resizing an existing page.

8. Performance, reliability, and cost

Each viewport case requires a page load and image capture, so a wider matrix increases browser time and output storage. Keep the matrix focused on meaningful breakpoint edges, content risks, and critical page templates. Reuse one browser process for a batch while creating a fresh page for each case; close pages and the browser in finally blocks so failures do not leave browser processes running.

For more reliable output, use a stable test environment, explicit timeouts, deterministic page data, and readiness checks tied to the content under test. Network idle can be convenient for mostly static pages, but it is not a guarantee that every visual dependency is ready. Browser screenshots also vary with browser versions, fonts, operating systems, and page state, so keep those inputs consistent when comparing runs.

Puppeteer itself is an open-source browser automation library; infrastructure cost depends on where and how often you run Chromium, the runtime and storage you choose, and the size of the screenshot matrix. The Puppeteer documentation does not provide a universal cost or speed benchmark for responsive screenshot suites.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. Make one GET request with a URL to receive a PNG, JPEG, WebP, or PDF. The API accepts viewport and device options, full-page captures, selector captures, custom CSS and JavaScript, wait conditions, and other capture settings. See the ScreenshotNeo 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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no cost and no card.

FAQ

Should I test every possible screen width?

No. Cover the site’s breakpoint transitions and the widths where important content is likely to wrap, overflow, or rearrange. The right set is specific to the page.

Does device emulation guarantee the same result as a real phone?

No. It applies browser device metrics and a user agent; it does not reproduce every hardware, operating system, or browser difference.

Can Puppeteer screenshots detect visual regressions by themselves?

Puppeteer captures images. A comparison step or visual review is still needed to decide whether a difference is a regression.

Which capture should I use for a responsive component?

Use an element screenshot for a single component, a viewport screenshot for visible layout, and a full-page screenshot when below-the-fold composition matters.

Official references