ScreenshotNeo

BlogHow-to

How to Emulate Mobile Devices in Puppeteer Screenshots

Use Puppeteer’s device descriptors or set viewport, scale, and touch options yourself. Learn how to capture mobile screenshots and troubleshoot common mismatches.

By the ScreenshotNeo team29 September 202610 min read

How to Emulate Mobile Devices in Puppeteer Screenshots

To emulate a mobile device in Puppeteer, apply a known device descriptor with page.emulate() before navigating, then capture the page with page.screenshot(). For custom setups, use page.setViewport() to control CSS-pixel dimensions, device scale, mobile viewport behavior, and touch support, and set a user agent separately when you need one. These settings reproduce browser-facing metrics and behavior; they do not guarantee a perfect match for every physical phone. Puppeteer Page API · Viewport API

1. Install Puppeteer and prepare a capture script

This example uses Node.js and the current Puppeteer package API. Install Puppeteer in a project directory; it downloads a compatible browser as part of installation in standard configurations. If your environment supplies Chrome separately, see Puppeteer’s installation guidance and configure the executable accordingly.

npm install puppeteer

Create mobile-shot.mjs. The device name below is an example: confirm that it exists in KnownDevices for the Puppeteer version installed in your project. The available descriptors can change between releases.

import puppeteer from 'puppeteer';

const target = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });

try {
  const page = await browser.newPage();
  const device = puppeteer.KnownDevices['iPhone 13'];
  if (!device) {
    throw new Error('Device descriptor not found in this Puppeteer version');
  }

  // Configure mobile metrics and user agent before navigation.
  await page.emulate(device);
  await page.goto(target, { waitUntil: 'networkidle2', timeout: 60000 });
  await page.screenshot({ path: 'mobile.png', fullPage: true });
  console.log('Saved mobile.png');
} finally {
  await browser.close();
}

Run it with:

node mobile-shot.mjs https://example.com

page.emulate(device) applies device metrics and the device user agent. It is a shortcut for setting the user agent and viewport. Emulate before goto(): a site can choose its initial layout or load different resources based on the browser configuration, and changing the viewport after navigation can produce a different result. Puppeteer also warns that changing mobile-related settings after navigation can cause a reload in some cases. Puppeteer Page API

2. Choose a device descriptor or configure metrics yourself

Use a built-in descriptor

The built-in KnownDevices collection is intended for Page.emulate(). Use the exact descriptor name found in the installed package, and fail clearly if it is missing instead of silently taking a desktop screenshot. To inspect names at runtime:

Apply the mobile configuration before navigation so the page loads into the intended browser environment.
Apply the mobile configuration before navigation so the page loads into the intended browser environment.
import puppeteer from 'puppeteer';

console.log(Object.keys(puppeteer.KnownDevices).slice(0, 20));

Do not assume a device descriptor is available solely because it appears in a blog post or an older example. The installed Puppeteer release is the authority for your project.

Set a custom viewport

Use explicit values when you need a particular responsive breakpoint or want to isolate individual settings. Width and height are CSS pixels. Device scale factor affects the relation between CSS pixels and rendered device pixels; the viewport API documents a default scale factor of 1. isMobile controls whether the page’s meta viewport tag is taken into account, while hasTouch enables touch support.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({
    width: 390,
    height: 844,
    deviceScaleFactor: 3,
    isMobile: true,
    hasTouch: true,
  });
  await page.setUserAgent(
    'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) ' +
    'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 ' +
    'Mobile/15E148 Safari/604.1'
  );
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'custom-mobile.png' });
} finally {
  await browser.close();
}

The user-agent string is an example, not a guarantee of impersonating a particular iPhone or Safari release. Keep it aligned with the browser and device behavior you intend to approximate. A viewport, scale factor, touch setting, and user agent each affect different parts of the page’s environment; changing just the width does not reproduce all of them.

Setting What it controls When to adjust it
width, height Viewport size in CSS pixels Responsive breakpoints and visible area
deviceScaleFactor Device scale factor High-density rendering or pixel-sensitive comparisons
isMobile Whether meta viewport is taken into account Testing pages designed for mobile viewport behavior
hasTouch Touch support on the viewport Pages with touch-specific behavior
User agent Browser identity string sent to the page Sites that branch on browser or device identity

These options and their defaults are documented in the Puppeteer Viewport API. Browser emulation is useful for responsive checks and repeatable screenshots, but it does not establish that hardware-specific behavior, a physical phone’s browser, or every sensor is identical.

3. Wait for the intended page state

A screenshot is only as useful as the state captured. Puppeteer’s screenshot guide demonstrates navigation with waitUntil: 'networkidle2' before capture. This can be a reasonable starting point for static pages, but it is not a universal “everything finished” signal: analytics, long polling, animations, client-side rendering, and lazy-loaded media can make readiness application-specific. Puppeteer screenshot guide

For a page with a known element that appears after rendering, wait for that selector:

await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('main article', { timeout: 15000 });
await page.screenshot({ path: 'article-mobile.png', fullPage: true });

For a short, fixed delay when the application has no better readiness signal:

await page.goto(target, { waitUntil: 'domcontentloaded' });
await new Promise(resolve => setTimeout(resolve, 1200));
await page.screenshot({ path: 'delayed-mobile.png' });

Use selector-based waits where possible: a fixed delay can waste time on fast pages and still be too short on slow ones. If images load only as they approach the viewport, scrolling the document or using the application’s own readiness indicator may be necessary before taking a full-page capture. Check the actual output when the page uses animation, video, or client-side data.

4. Select the screenshot area and format

By default, page.screenshot() captures the current viewport. Use fullPage: true for the full document, or clip for a specific rectangle. These are different capture choices: a full-page image can be very tall, while a clipped image is bounded to the requested region. See Puppeteer ScreenshotOptions.

Choose a viewport, full-page, clipped, or element capture based on the output you need.
Choose a viewport, full-page, clipped, or element capture based on the output you need.
// Viewport image
await page.screenshot({ path: 'viewport.png' });

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

// A rectangle in page coordinates
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 120, width: 390, height: 500 },
  captureBeyondViewport: true,
});

// JPEG output; quality applies to JPEG/WebP, not PNG
await page.screenshot({ path: 'mobile.jpg', type: 'jpeg', quality: 82 });

// Transparent background when the page supports transparency
await page.screenshot({ path: 'transparent.png', omitBackground: true });

Puppeteer documents PNG as the default screenshot type. Quality is in the range 0–100 and does not apply to PNG. Choose PNG when crisp text or exact pixels matter; choose JPEG or WebP when a smaller raster file is more useful and some lossy compression is acceptable. Transparent output hides the default white background, but page elements can still paint their own backgrounds.

For one component rather than the full page, use an element handle:

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

The screenshot guide notes that Puppeteer attempts to scroll an element into view before capturing it. For a selector that matches several nodes, make the selection explicit so the output is deterministic.

5. Make the capture repeatable

  1. Pin your Puppeteer version. Device descriptors and API details are version-sensitive; use the project lockfile and check the installed documentation.
  2. Apply emulation before navigation. Set the same viewport, scale, mobile, touch, and user-agent values for every run.
  3. Wait for a meaningful condition. Prefer a page-specific selector or state over an arbitrary delay.
  4. Keep output choices explicit. Name the path, full-page behavior, format, and quality instead of relying on defaults in a comparison pipeline.
  5. Record the inputs. If you compare screenshots over time, store target URL, device descriptor or metrics, browser/package version, and wait condition alongside the image.

Full-page captures at a high device scale factor can produce large images and consume more memory than viewport captures. If your job only checks a mobile navigation bar or a hero section, capture that region or element instead of the full document. For visual regression, keep the same browser version, font availability, locale, timezone, page data, and capture timing; otherwise differences may reflect the environment rather than a layout change.

6. Troubleshooting common problems

Symptom Likely cause Fix
“Device descriptor not found” or undefined device The descriptor name is absent or differs in the installed release. Inspect Object.keys(puppeteer.KnownDevices); use an available name or define custom viewport and user agent.
Desktop layout in the image Emulation ran after navigation, mobile behavior is disabled, or the site does not use responsive CSS. Emulate before goto(); set isMobile: true for custom metrics; inspect the site’s breakpoints and meta viewport.
Content appears zoomed out or unusually narrow The page’s meta viewport behavior differs from the expected mobile setup. Confirm isMobile and check the page’s viewport meta tag; use a known device descriptor for a standard configuration.
Touch interaction is missing The viewport was configured without touch support. Set hasTouch: true. This enables touch support but does not make every input device behavior identical to a phone.
Screenshot is blank or missing late content The page had not reached the desired state, a request failed, or content is lazy-loaded. Wait for a page-specific selector, inspect navigation and console errors, and scroll or trigger the lazy content before capture.
Full-page output is much taller than expected The document itself is tall, or content expands as it renders. Check document dimensions and capture a specific element, viewport, or clip if the entire page is not needed.
Unexpected reload after settings change Some mobile viewport settings can cause a reload when changed after navigation. Set mobile metrics before navigating and avoid changing isMobile or hasTouch mid-capture.
PNG ignores quality Puppeteer quality does not apply to PNG. Use JPEG or WebP when quality-based compression is desired, or keep PNG for lossless output.

Or skip the browser setup

ScreenshotNeo takes a screenshot through one GET request. The API accepts a URL and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options.

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

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

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

7. cURL, Python, and Node.js options

Puppeteer is a Node.js library, so cURL and Python do not directly control its page instance. Use the Node.js script above for Puppeteer emulation. These examples call ScreenshotNeo’s screenshot API when you want a managed capture request instead of launching a local browser. Keep your API key private; do not embed it in public client-side code.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.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));

8. Performance, reliability, and cost considerations

For self-hosted Puppeteer, the main work is starting or reusing a browser, loading the target page, waiting for a stable state, and encoding the output. Reusing a browser process across multiple captures can avoid repeated startup cost, but isolate pages and close them after each job so failures do not accumulate. Set navigation and selector timeouts, close the browser in a finally block, and record failed URLs for retry. Retry transient navigation failures with a limit and backoff; repeating a deterministic page error indefinitely wastes resources.

There is no universal wait setting that makes every website both fast and complete. networkidle2 can wait too long for pages with persistent network activity, while a short fixed delay can capture before client-side rendering finishes. A selector-based readiness condition usually gives the best balance when the page has a reliable marker. Keep full-page and high-scale captures for cases that need them, since output dimensions and memory use grow with the captured area and scale.

Self-hosting means you provide the runtime, browser, and compute resources; Puppeteer documentation does not specify a per-screenshot service price. ScreenshotNeo’s published plan facts for this article are: Free 1,000 shots/month with no card; Starter $5 for 3,000; Growth $15 for 15,000; Pro $39 for 60,000; Scale $99 for 250,000; Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Only clean shots are billed; responses identify the page verdict and billing status with X-Page-Verdict and X-Billed headers. See the docs for configuration and response details.

9. Frequently asked questions

Does Puppeteer emulate a real phone?

It configures browser-facing metrics and a user agent. That is useful for responsive layouts and screenshot workflows, but the documented settings do not promise complete physical-device fidelity.

Should I use a device descriptor or custom values?

Use a descriptor for a standard preset that exists in your installed version. Use explicit viewport values when you need a precise breakpoint, custom scale, or a controlled experiment.

Can I capture just one component?

Yes. Select the element and call its screenshot() method. Use page.screenshot() for the viewport, full document, or a clipped region.

Why does my image differ from a phone screenshot?

The viewport and user agent are only some of the browser environment. Browser version, fonts, page state, timing, and physical-device behavior can also affect pixels and interactions.