ScreenshotNeo

BlogHow-to

How to Capture Responsive Website Screenshots with Puppeteer

Capture a page at custom responsive viewports or emulated devices with Puppeteer, then save viewport, full-page, clipped, or element screenshots.

By the ScreenshotNeo team4 October 20269 min read

Use Puppeteer’s Page API: set the viewport or emulate a device before navigating, load the page to the state you need, then call page.screenshot(). For a responsive comparison, repeat that sequence for each viewport and save each result to a different file. Use fullPage, clip, or an element handle when you need more than the visible viewport. See the Puppeteer screenshots guide, setViewport() API, and emulate() API.

1. Install Puppeteer and capture a responsive viewport

Install Puppeteer in a Node.js project. The full puppeteer package downloads a compatible browser during installation. If your deployment manages Chrome separately, review Puppeteer’s installation guidance and configure the executable as appropriate for that environment.

npm install puppeteer

Save the following as capture.mjs and run it with node capture.mjs. It uses a mobile-sized viewport, waits for networkidle2, and writes a full-page PNG.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({
    width: 390,
    height: 844,
    deviceScaleFactor: 1,
  });
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2',
    timeout: 45_000,
  });
  await page.screenshot({
    path: 'example-mobile.png',
    fullPage: true,
  });
} finally {
  await browser.close();
}

setViewport() controls the CSS viewport dimensions; it does not by itself make the browser a particular phone. Set it before goto(): changing viewport properties can resize the page and, in some cases when mobile or touch properties change, reload it. Puppeteer documents this ordering and behavior.

2. Capture a responsive breakpoint matrix

For custom widths, make one page per configuration, set each viewport before navigation, and capture with a unique path. A separate page per size avoids accidentally reusing a page state after resizing. These dimensions are examples; substitute the breakpoints relevant to your layout.

import puppeteer from 'puppeteer';

const target = 'https://example.com';
const viewports = [
  { name: 'mobile', width: 390, height: 844, deviceScaleFactor: 1 },
  { name: 'tablet', width: 768, height: 1024, deviceScaleFactor: 1 },
  { name: 'desktop', width: 1440, height: 900, deviceScaleFactor: 1 },
];

const browser = await puppeteer.launch();
try {
  for (const viewport of viewports) {
    const page = await browser.newPage();
    await page.setViewport(viewport);
    await page.goto(target, {
      waitUntil: 'networkidle2',
      timeout: 45_000,
    });
    await page.screenshot({
      path: `example-${viewport.name}.png`,
      fullPage: true,
    });
    await page.close();
  }
} finally {
  await browser.close();
}

If you are comparing results across runs, keep the URL, browser and Puppeteer versions, viewport dimensions, device scale factor, capture scope, and page state consistent. Those are practical controls for interpretable comparisons; they do not guarantee identical pixels across operating systems or browser builds.

3. Choose custom viewport sizing or device emulation

Custom viewport

Choose setViewport() when you want exact width and height values or are checking CSS breakpoints. deviceScaleFactor controls the device pixel ratio. A value of 1 is useful for ordinary CSS-pixel captures; a larger value can produce denser output. It increases output pixel dimensions and may increase memory and file size.

await page.setViewport({
  width: 1280,
  height: 800,
  deviceScaleFactor: 2,
});

Known device

Use KnownDevices and page.emulate() when the site responds to device metrics and user agent as well as viewport size. Emulation is a shortcut for setting both the viewport and user agent, so it is more representative than changing width alone for sites with user-agent-specific behavior. Device names available depend on the installed Puppeteer version; inspect that version’s KnownDevices reference.

import puppeteer, { KnownDevices } from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.emulate(KnownDevices['iPhone 17 Pro']);
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'example-device.png', fullPage: true });
} finally {
  await browser.close();
}

Apply emulation before navigation. A named-device preset helps reproduce its configured metrics and user agent; it is not a physical device and does not reproduce every hardware, operating system, network, or browser behavior.

4. Pick the screenshot scope

Need Option Example
What is visible in the current viewport Default page screenshot await page.screenshot({path: 'view.png'});
The whole document vertically fullPage: true await page.screenshot({path: 'full.png', fullPage: true});
A rectangular region clip with x, y, width, and height await page.screenshot({path: 'region.png', clip: {x: 0, y: 0, width: 600, height: 400}});
A specific component Element handle screenshot await (await page.waitForSelector('main article')).screenshot({path: 'article.png'});

The visible viewport is the most direct representation of one screen position. A full-page screenshot can be very tall and is not the same as a viewport capture. For a component, element screenshots scroll the element into view if needed; a detached element causes an error. See the element screenshot API.

// A rectangle in page coordinates
await page.screenshot({
  path: 'header-region.png',
  clip: { x: 0, y: 0, width: 1200, height: 240 },
});

// A single element, selected after it appears
const card = await page.waitForSelector('.product-card', { timeout: 10_000 });
if (!card) throw new Error('Product card did not appear');
await card.screenshot({ path: 'product-card.png' });

5. Wait for the page state you actually need

Navigation completion and visual readiness are different. Puppeteer supports navigation wait conditions such as load, domcontentloaded, networkidle0, and networkidle2. A network-idle condition can be unsuitable for pages with persistent requests, while a page may still reveal content after navigation. Choose the least restrictive condition that reliably reaches your desired state, then wait for a meaningful selector or application-specific condition if necessary.

await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',
  timeout: 45_000,
});
await page.waitForSelector('[data-page-ready="true"]', { timeout: 15_000 });
await page.screenshot({ path: 'ready.png', fullPage: true });

If the page has no readiness marker, a bounded delay can be used as a fallback, but it is less reliable than waiting for a state that indicates the content is ready:

await page.goto('https://example.com', { waitUntil: 'load' });
await new Promise(resolve => setTimeout(resolve, 1_000));
await page.screenshot({ path: 'after-delay.png' });

For lazy-loaded content, a full-page screenshot does not necessarily mean every off-screen image has loaded. If needed, scroll through the page before capture and wait for images to complete. This is site-dependent; do not assume one generic wait guarantees every page’s lazy content is ready.

6. Screenshot output options

Puppeteer’s ScreenshotOptions include the following useful settings. Confirm exact format support against the installed Puppeteer version.

Option Use Notes
path Save the result to disk Format can be inferred from extension when a path is provided; relative paths resolve from the process working directory.
type Select an image format PNG is the default in the documented API. Verify supported formats in your version.
quality Adjust lossy image quality Applies to non-PNG output; documented range is 0–100.
fullPage Capture the full page Defaults to false.
clip Capture a rectangle Use page coordinates and positive dimensions.
omitBackground Allow transparent background Hides the default white background where the page content permits transparency.
encoding Return base64 or binary data Binary is the default; use base64 when that representation is needed.
// JPEG with lossy quality; quality is not used for PNG
await page.screenshot({ path: 'preview.jpg', type: 'jpeg', quality: 85 });

// Transparent canvas when the page has no opaque background
await page.screenshot({ path: 'overlay.png', omitBackground: true });

7. cURL, Python, and Node.js with ScreenshotNeo

If your goal is to request a screenshot rather than manage a local browser, ScreenshotNeo is a hosted website screenshot API. One GET request returns an image or PDF. Its parameter names are compatible with those used by other screenshot APIs, which can make switching easier. The ScreenshotNeo API documentation describes the available options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

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 can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. It also provides an MCP server for AI agents, with take_screenshot, get_page_info, and capture_pdf tools. Plans include 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 screenshots.

Start with 1,000 free screenshots a month, no card required.

8. Performance, reliability, and cost

  • Reuse the browser. Launching Chrome is setup work. For a batch, launch once and create pages as needed; close each page and always close the browser in a finally block.
  • Bound work. Give navigation and selector waits timeouts. Process a controlled number of pages concurrently; too many browser pages can increase memory use and make captures less predictable.
  • Keep images manageable. Viewport captures usually use less memory and disk than full-page captures. Retina scale increases the number of output pixels. Choose JPEG quality for smaller photographic output when supported; PNG quality has no effect.
  • Make comparisons reproducible. Record the viewport, device preset, browser/package version, wait condition, URL, and screenshot scope with each result.
  • Use deterministic readiness. A selector or application-ready state is generally more repeatable than an arbitrary sleep. Pages with long polling may never become network-idle.
  • Local capture costs. Puppeteer itself is a library, but running it consumes compute, memory, storage, and maintenance time for browser installation and updates. No benchmark or fixed per-image cost is implied; measure your own workload.
  • Hosted capture costs. ScreenshotNeo offers 1,000 free shots monthly, then listed plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Every feature is on every plan. Review current plan details before choosing.

9. Troubleshooting

Symptom Likely cause Fix
Mobile layout is not captured Viewport was set after navigation, or only width was changed while the site checks user agent. Set the viewport before goto(); use page.emulate(KnownDevices[...]) for a known device profile.
Navigation timeout The page is slow, has persistent network activity, or the selected wait condition never resolves. Choose a more suitable waitUntil, set a justified timeout, then wait for a page-specific selector.
Screenshot is blank or missing late content Capture happened before the application rendered or before lazy content loaded. Wait for a meaningful selector or app-ready condition; scroll for lazy content if the page requires it.
Element screenshot throws or selector times out The selector does not match, the element appears later, or the node was detached before capture. Use a stable selector, wait for it, and reacquire it immediately before the screenshot.
Clipped screenshot fails or looks wrong Clip coordinates or dimensions are invalid or refer to an unexpected page region. Check the clip origin and positive width/height; use an element screenshot for a component instead.
Output format or quality is unexpected File extension, type, or installed version support differs from assumptions. Set a supported type explicitly, use a matching extension, and remember quality only affects non-PNG output.
Transparent output still has an opaque region The page itself paints an opaque background. Use omitBackground: true and ensure the page element/background styling is transparent.
Browser launch fails in a container The runtime may lack browser dependencies or the expected executable. Follow Puppeteer’s installation instructions for the environment and verify the browser executable and runtime dependencies.

10. FAQ

Does responsive screenshotting require a real phone?

No. A custom viewport or Puppeteer device emulation can capture common responsive layouts. Use a real-device test when the behavior depends on hardware or platform details the emulation does not represent.

Should I take one screenshot per breakpoint?

For a breakpoint review, capture at widths around the breakpoints that matter to your CSS, plus representative tablet and desktop sizes. A few carefully selected widths are more actionable than an arbitrary large matrix.

Can I use Puppeteer screenshots in CI?

Yes, provided the CI environment can run the browser and has enough resources. Pin the browser/package environment and wait for stable page state to reduce unrelated visual changes.

Is a full-page screenshot always better?

No. Use it when you need the entire document. For a visual check of what fits on a screen, capture the viewport; for one widget, capture the element.