ScreenshotNeo

BlogHow-to

How to capture a mobile website screenshot with Puppeteer device emulation

Use Puppeteer’s device presets to capture a mobile website screenshot. Learn setup, full-page and element captures, reliable waits, and common fixes.

By the ScreenshotNeo team4 October 20268 min read

Puppeteer can capture a mobile website screenshot by applying a device preset before navigating to the page, then calling page.screenshot(). The preset supplies a user agent and viewport metrics; this is browser device emulation, not a capture from a physical phone.

1. Install Puppeteer and capture a mobile page

Use a current Node.js release and install Puppeteer in a project directory. The standard puppeteer package downloads a compatible browser for local automation.

npm init -y
npm install puppeteer

Save this as screenshot.mjs. It uses Puppeteer’s documented KnownDevices preset pattern, emulates before navigation, waits for navigation to settle, and writes a full-page PNG.

import puppeteer, {KnownDevices} from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.emulate(KnownDevices['iPhone 17 Pro']);
  await page.goto(url, {waitUntil: 'networkidle2', timeout: 60_000});
  await page.screenshot({path: 'mobile.png', fullPage: true});
  console.log('Saved mobile.png');
} finally {
  await browser.close();
}
node screenshot.mjs https://example.com

Replace the preset with the device that matches the viewport and user agent relevant to your test. See Puppeteer’s Page.emulate() and KnownDevices documentation.

2. Choose a device preset or define a custom device

page.emulate(device) is a convenience for setting a user agent and viewport. Puppeteer notes that emulation resizes the page, and some sites do not expect a phone-sized resize; apply it before page.goto() so the site initializes at the intended dimensions. The device data consists of a user-agent string and viewport settings. See Device.

To inspect available presets in the installed Puppeteer version:

import {KnownDevices} from 'puppeteer';
console.log(Object.keys(KnownDevices));

Use a custom device object when a named preset does not match your test. For example:

const customPhone = {
  name: 'Custom phone',
  userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
  viewport: {
    width: 390,
    height: 844,
    deviceScaleFactor: 3,
    isMobile: true,
    hasTouch: true,
    isLandscape: false
  }
};

await page.emulate(customPhone);

For repeatable comparisons, record the exact preset or custom viewport, user agent, orientation, and scale factor alongside the screenshot. Emulation does not establish that a physical device, operating-system browser, or every device-specific capability behaves identically.

3. Select viewport, full-page, clipped, or element capture

page.screenshot() captures the visible viewport by default. Use fullPage: true for the full document, or clip to capture a specified rectangle. The output type defaults to PNG and can be inferred from the filename extension. Puppeteer documents screenshot options such as image type, supported-format quality, and transparent background via omitBackground. See the screenshot API and ScreenshotOptions.

Viewport screenshot

await page.screenshot({path: 'mobile-viewport.png'});

Full-page screenshot

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

Clip a rectangle

await page.screenshot({
  path: 'mobile-region.png',
  clip: {x: 0, y: 120, width: 390, height: 500}
});

Clip coordinates are page screenshot coordinates. Ensure the clip fits the rendered page and use dimensions appropriate to the emulated viewport and desired region.

Capture one element

const card = await page.waitForSelector('.product-card', {visible: true});
if (!card) throw new Error('Product card was not found');
await card.screenshot({path: 'product-card.png'});

Puppeteer’s element screenshot scrolls the element into view when needed. If the element detaches from the document before capture, the operation errors; wait for a stable selector or locate the element again. See ElementHandle.screenshot().

4. Wait for the page content that matters

networkidle2 is a useful navigation wait condition shown in Puppeteer’s screenshot guide, but it does not guarantee that application data, animations, fonts, or lazy images are visually ready. Choose readiness based on the page under test. The Puppeteer screenshots guide demonstrates navigation and capture.

Wait for a page-specific selector

await page.goto(url, {waitUntil: 'domcontentloaded'});
await page.waitForSelector('[data-test="results-ready"]', {visible: true});
await page.screenshot({path: 'results.png', fullPage: true});

Wait for a known delay

Use a delay only when the application has no reliable readiness signal, and keep it tied to the behavior being tested.

await page.goto(url, {waitUntil: 'networkidle2'});
await new Promise(resolve => setTimeout(resolve, 800));
await page.screenshot({path: 'after-delay.png'});

Handle lazy-loaded content deliberately

A full-page screenshot does not guarantee that every image loaded through scroll-triggered lazy loading has been requested. For pages that load content as the viewport moves, scroll through the document before the final capture, then wait for the relevant images or page state. A simple scroll pass is:

await page.evaluate(async () => {
  const step = Math.max(300, window.innerHeight);
  for (let y = 0; y < document.body.scrollHeight; y += step) {
    window.scrollTo(0, y);
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  window.scrollTo(0, 0);
});
await page.screenshot({path: 'lazy-loaded.png', fullPage: true});

Adjust the wait or replace it with a site-specific condition if image requests or content rendering take longer. Puppeteer’s screenshot option controls the capture extent; the page itself determines when its content is ready.

5. Configure image output

Common options include path, type, quality where supported, fullPage, clip, and omitBackground. PNG is the default. JPEG and WebP are useful when smaller files matter; quality applies to supported lossy formats. Check the installed Puppeteer version’s options reference for the supported values.

await page.screenshot({
  path: 'mobile.webp',
  type: 'webp',
  quality: 80,
  fullPage: true
});

For an image with a transparent page background, use PNG and omit the browser background:

await page.screenshot({
  path: 'transparent.png',
  omitBackground: true
});

6. Run captures reliably in scripts and CI

  • Always close the browser in a finally block so failed navigation or capture does not leave a browser process running.
  • Set an explicit navigation timeout for pages that may be slow, and handle timeout failures as failed captures rather than silently using a partial page.
  • Use a selector or application readiness signal when the visual state matters more than network quiet.
  • Keep browser and Puppeteer versions controlled in repeatable jobs, and use the same device preset and options for each comparison.
  • Write outputs to unique paths when capturing multiple URLs or devices to avoid overwriting files.
  • For very tall pages, consider viewport or element screenshots if a single full-page image becomes unwieldy for downstream review or storage.

A simple multi-device loop can reuse one browser while creating a fresh page per preset:

import puppeteer, {KnownDevices} from 'puppeteer';

const browser = await puppeteer.launch();
try {
  for (const [name, device] of [
    ['iphone', KnownDevices['iPhone 17 Pro']],
    ['pixel', KnownDevices['Pixel 9']]
  ]) {
    const page = await browser.newPage();
    await page.emulate(device);
    await page.goto('https://example.com', {waitUntil: 'networkidle2', timeout: 60_000});
    await page.screenshot({path: `${name}.png`, fullPage: true});
    await page.close();
  }
} finally {
  await browser.close();
}

Preset names vary across Puppeteer releases; inspect Object.keys(KnownDevices) if one in a copied example is undefined.

7. Troubleshooting

Symptom Likely cause Fix
The page looks like desktop despite a phone-sized capture Emulation ran after navigation, or the viewport was changed after the site initialized. Call page.emulate(device) before page.goto(). Confirm the chosen preset viewport and user agent.
KnownDevices[... ] is undefined The preset name is not present in the installed Puppeteer release or has a different spelling. Print Object.keys(KnownDevices) and select an available name, or define the device object explicitly.
Navigation times out The page did not reach the selected wait condition before the timeout, often because it keeps network connections open. Choose a navigation condition suited to the page, set a justified timeout, then wait for a page-specific selector. Do not treat a relaxed wait as proof that the page is ready.
Screenshot is blank or content is missing Capture happened before app rendering, images, or lazy content finished loading. Wait for a visible content selector or application-ready state; scroll to trigger lazy loads and wait for the relevant content.
Element screenshot reports a detached node The page replaced or removed the element between selection and capture. Wait for the element to stabilize and query it again immediately before calling screenshot().
Clip capture fails or omits the target The rectangle is outside the rendered page or does not match the intended coordinates. Check x/y and width/height against the page dimensions; use an element screenshot for a moving or content-sized target.
Output format or file size is unexpected The extension, explicit type, or quality option does not match the desired format. Set type explicitly when needed, use a compatible extension, and apply quality only to supported formats.

8. Performance, reliability, and cost

Browser startup and page rendering are the main work in a local capture. Reusing a browser for a sequence of pages avoids repeated launches, while closing each page after capture limits leftover page state. Full-page images can take more memory and produce larger files than viewport or element captures. Choose the smallest capture extent and image format that meet the task.

Reliability depends on the target site and its readiness signals. A navigation event or network-idle state is not a visual correctness check; use stable page-specific selectors and capture failures explicitly. Puppeteer itself is browser automation software, so compute and browser maintenance are your responsibility when running it in CI or a server.

Local Puppeteer has no per-screenshot API charge, but it uses the machine’s compute, storage, and maintenance time. If you prefer a hosted screenshot endpoint, ScreenshotNeo accepts a URL and returns an image or PDF; its usage is plan-based.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A GET request captures a URL without installing or maintaining a browser in your script. The returned format can be PNG, JPEG, WebP, or PDF. 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}`);
  • Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
  • An MCP server gives AI agents tools for screenshots, page information, and PDF capture.
  • The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

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

Frequently asked questions

Does Puppeteer device emulation use a real phone?

No. It applies browser user-agent and viewport metrics for an emulated capture. It does not establish that the result matches every physical handset or mobile browser behavior.

Should I use full-page screenshots for responsive testing?

Use a viewport capture when the target is the visible mobile screen. Use full-page when reviewing the whole document, and an element capture when a specific component is the subject.

Why does a full-page image still have missing images?

Some sites load images only as they approach the viewport. Trigger those loads and wait for the page’s content to settle before taking the full-page capture.

Can I compare screenshots across devices?

Yes. Keep the URL, device preset, orientation, readiness condition, and screenshot options consistent, and interpret differences as results of the selected browser emulation.