How to capture mobile web screenshots with Playwright’s device descriptors
Use Playwright device descriptors to emulate a mobile browser, choose screenshot dimensions and scale, and capture a viewport or full page.
Use Playwright’s devices registry to apply a named device descriptor to a browser context, then capture the page with page.screenshot(). A descriptor configures browser emulation, including viewport, screen, user agent, and touch behavior; it does not mean the page ran on a physical phone.
The examples below use the iPhone 13 descriptor documented by Playwright. Device names can vary with the Playwright version installed in your project, so check that version’s devices registry if the name is unavailable.
1. Capture a page with a mobile device descriptor
Install Playwright and its browser if they are not already available:
npm install playwright
npx playwright install chromium
Save this as mobile-screenshot.mjs and run node mobile-screenshot.mjs:
import { chromium, devices } from 'playwright';
const device = devices['iPhone 13'];
if (!device) {
throw new Error('The iPhone 13 descriptor is not available in this Playwright version.');
}
const browser = await chromium.launch();
try {
const context = await browser.newContext({ ...device });
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({
path: 'mobile-page.png',
fullPage: true,
scale: 'css',
});
await context.close();
} finally {
await browser.close();
}
networkidle can be unsuitable for pages with persistent network activity. If navigation does not settle, use waitUntil: 'domcontentloaded' or 'load', then wait for a specific selector that indicates the content you need is ready.
2. Configure a Playwright Test project
For repeatable test runs, spread the descriptor into a Playwright Test project’s use settings. Save this configuration as playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'Mobile Safari emulation',
use: { ...devices['iPhone 13'] },
},
],
});
This is a browser configuration for that project. Choose the browser associated with the descriptor when you need to match its intended browser engine, and check the installed registry for available names.
3. Choose the viewport and screenshot extent
A descriptor provides a viewport. To use custom dimensions, add viewport after the descriptor spread so your value takes precedence:
const context = await browser.newContext({
...devices['iPhone 13'],
viewport: { width: 390, height: 844 },
});
Those dimensions are your custom test configuration, not an unmodified device preset. You can also resize an existing page:
await page.setViewportSize({ width: 390, height: 844 });
| Capture choice | How to set it | What it means |
|---|---|---|
| Viewport screenshot | Omit fullPage or set it to false |
Captures the visible viewport. |
| Full-page screenshot | fullPage: true |
Captures the full scrollable page. |
| CSS-pixel scale | scale: 'css' |
One output image pixel per CSS pixel. |
| Device-pixel scale | scale: 'device' |
Uses device-pixel resolution; high-DPI output may be larger. This is the documented default. |
Set the scale explicitly when image dimensions need to stay consistent across devices. Set path to save a file; Playwright infers the image type from its extension.
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'entire-page.png', fullPage: true });
await page.screenshot({ path: 'review.png', fullPage: true, scale: 'css' });
4. Understand what the descriptor emulates
Device descriptors supply a bundle of browser settings. Depending on the descriptor, this can include user agent, screen size, viewport, and touch capability. The isMobile option affects whether the page’s meta viewport tag is taken into account and enables touch events; it is included in the device preset. Keep the preset’s mobile flags intact when you want its documented configuration.
A preset can also make platform assumptions. For example, Playwright’s Desktop Chrome preset supplies a Windows user agent. If you want the host platform’s user agent instead, unset the preset’s userAgent when creating the context:
const { userAgent, ...hostAgentDevice } = devices['Desktop Chrome'];
const context = await browser.newContext(hostAgentDevice);
That example is about a desktop preset; do not remove a mobile descriptor’s user agent without a reason. In reports, describe the result as a screenshot from a configured Playwright browser context. Validate on actual hardware separately when real-device behavior matters.
5. Make captures repeatable
- Pin the project’s Playwright dependency. The registry is tied to the installed version. Check the actual descriptor exists instead of assuming every version has the same list.
- Set the device and overrides in one place. Apply the descriptor first, then put deliberate custom settings after it.
- Wait for the content you intend to capture. Prefer a meaningful selector or a suitable navigation event over an arbitrary delay. Sites with long-lived requests may never reach network idle.
- Choose screenshot extent and scale explicitly. This prevents a viewport capture from being mistaken for a full page and avoids relying on the device-scale default.
- Keep the output format clear. Use a matching filename extension such as
.pngso Playwright can infer the image type.
6. Troubleshoot common problems
| Symptom | Likely cause | Fix |
|---|---|---|
devices['iPhone 13'] is undefined |
The installed Playwright version does not contain that registry entry, or the name differs. | Inspect the installed devices object and use a name it provides. Keep the guard in the script to fail with a useful message. |
| The page looks desktop-sized | The context did not receive the descriptor, or a later viewport setting replaced its viewport. | Pass { ...devices['iPhone 13'] } to newContext(). Check overrides and their order. |
| The screenshot cuts off below the fold | The screenshot defaults to the current viewport. | Set fullPage: true for the full scrollable page. |
| The output is unexpectedly large | The default scale is 'device', which can produce more pixels on high-DPI configurations. |
Use scale: 'css' for one image pixel per CSS pixel. |
| Navigation hangs while waiting for network idle | Analytics, polling, streaming, or other persistent requests keep the network active. | Use domcontentloaded or load, then wait for a page-specific selector. |
| Mobile layout differs from a real phone | The descriptor simulates browser characteristics; it is not a physical-device run. | Use the screenshot for configured emulation and validate hardware-dependent behavior on an actual device. |
| Touch or viewport behavior differs from expectations | Mobile flags such as isMobile or the page’s meta viewport handling may have been changed. |
Retain the descriptor’s mobile options unless the test intentionally varies them. |
7. Performance, reliability, and cost
Screenshot cost and run time depend on the page and capture configuration. A full-page capture can produce a taller, larger image than a viewport capture; device-pixel scale can increase output dimensions. Choose only the extent and scale your task needs, and wait for the relevant content rather than all network activity when the page keeps requests open.
For reliable comparisons, use the same Playwright version, descriptor, browser engine, viewport overrides, wait condition, and scale across runs. A successful emulated capture is useful for responsive review, but it does not establish behavior on physical hardware. The dossier documents no benchmark or fixed performance figure for this workflow.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot or PDF, and its parameter names also work with those used by other screenshot APIs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Python and Node.js versions are below:
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())));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does a device descriptor make the capture a real phone screenshot?
No. It configures browser emulation. Use a separate real-device validation method when hardware behavior matters.
Should I use CSS scale or device scale?
Use CSS scale when you want one output pixel per CSS pixel and more comparable image dimensions. Device scale follows device-pixel resolution and is the documented default.
Can I customize the preset’s viewport?
Yes. Put your viewport after the descriptor spread in context options, or call page.setViewportSize(). The result is a custom configuration.


