ScreenshotNeo

BlogHTML to image & PDF

How to Scale Puppeteer PDFs to the Emulated Device Viewport

Keep Puppeteer’s viewport, CSS media, PDF paper size, and rendering scale separate so PDFs match an emulated device reliably.

By the ScreenshotNeo team30 September 20269 min read

How to Scale Puppeteer PDFs to the Emulated Device Viewport

To make a Puppeteer PDF match an emulated device viewport, configure four independent controls: the viewport width and height in CSS pixels, the viewport device scale factor, the CSS media type, and the PDF page dimensions and rendering scale. A device pixel ratio does not set PDF paper size. Start with the device layout, choose whether the PDF should use screen or print CSS, then define paper dimensions with format, width/height, or CSS @page.

Puppeteer’s page.pdf() uses the print CSS media type by default. If the PDF must look like the screen rendering, call await page.emulateMediaType('screen') first. The deviceScaleFactor belongs to viewport emulation, while PDFOptions scale controls PDF rendering and accepts values from 0.1 to 2 with a default of 1. There is no universal formula that converts every device profile into a correct PDF scale; validate representative pages in the Chromium version you deploy.

1. Understand the four sizing controls

Most viewport-to-PDF problems come from treating separate settings as one. Puppeteer exposes them at different stages of the capture pipeline.

Viewport metrics, CSS media, paper size, and PDF scale are separate controls.
Viewport metrics, CSS media, paper size, and PDF scale are separate controls.
Control What it changes What it does not change
width and height in setViewport() CSS layout viewport dimensions PDF paper size or output DPI
deviceScaleFactor Emulated device pixel ratio used by the page PDF paper dimensions
emulateMediaType() Whether CSS @media screen or @media print rules are active Viewport dimensions and paper size
width, height, or format in page.pdf() PDF paper dimensions Browser viewport layout
preferCSSPageSize Whether CSS @page size takes priority Device emulation metrics
scale in page.pdf() PDF page rendering scale, 0.1–2 CSS viewport width and height

The Viewport interface documents CSS-pixel dimensions and deviceScaleFactor separately. The PDFOptions interface documents paper sizing, preferCSSPageSize, and PDF scale separately.

2. A complete Puppeteer implementation

This runnable Node.js example emulates a phone before navigation, activates screen styles, and creates a PDF whose explicit dimensions match the CSS viewport. Save it as capture-device-pdf.mjs, install Puppeteer with npm install puppeteer, and run it with a URL argument.

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();

  // Width and height are CSS pixels. deviceScaleFactor is independent.
  await page.setViewport({
    width: 390,
    height: 844,
    deviceScaleFactor: 3,
    isMobile: true,
    hasTouch: true
  });

  // Emulate before navigation so responsive code sees the intended device.
  await page.goto(target, {
    waitUntil: 'networkidle2',
    timeout: 60000
  });

  // page.pdf() defaults to print CSS. Use screen CSS when that is desired.
  await page.emulateMediaType('screen');

  await page.pdf({
    path: 'device-viewport.pdf',
    width: '390px',
    height: '844px',
    preferCSSPageSize: true,
    scale: 1,
    printBackground: true,
    margin: {top: '0px', right: '0px', bottom: '0px', left: '0px'}
  });
} finally {
  await browser.close();
}

Page.emulate() is a shortcut for applying a device’s user agent and viewport. Puppeteer recommends doing device emulation before navigation because some sites do not expect the viewport to change after loading. See the Page.emulate() reference.

Use a device preset

When you need a named phone or tablet profile, import a device descriptor and emulate it before calling goto.

import puppeteer from 'puppeteer';
import {devices} from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.emulate(devices['iPhone 13']);
  await page.goto('https://example.com', {waitUntil: 'networkidle2'});
  await page.emulateMediaType('screen');
  await page.pdf({
    path: 'iphone-layout.pdf',
    width: '390px',
    height: '844px',
    printBackground: true,
    scale: 1
  });
} finally {
  await browser.close();
}

A preset sets a user agent and viewport, but it does not automatically decide the PDF paper dimensions you want. Choose those dimensions for your output format.

3. Choose the PDF page size

Explicit width and height

For a one-screen PDF, set width and height to the same physical CSS lengths used by the viewport. CSS units such as px, in, cm, and mm are accepted. This keeps the page shape predictable when the target is a device-sized sheet.

await page.pdf({
  path: 'viewport.pdf',
  width: '390px',
  height: '844px',
  scale: 1,
  printBackground: true
});

Standard paper with format

Use format when the deliverable is A4, Letter, Legal, or another supported paper preset. When format is present, it takes priority over width and height. A phone viewport can still be used to drive responsive layout, but the PDF will be laid out onto the selected paper.

await page.pdf({
  path: 'a4.pdf',
  format: 'A4',
  printBackground: true,
  margin: {top: '12mm', right: '12mm', bottom: '12mm', left: '12mm'}
});

Let CSS @page define the paper

If the application owns the print design, define the size in CSS and set preferCSSPageSize: true. The documented default is false, which scales content to fit the requested paper. With true, the CSS page size takes priority.

@page {
  size: 390px 844px;
  margin: 0;
}

@media print {
  body { margin: 0; }
}
await page.emulateMediaType('screen');
await page.pdf({
  path: 'css-sized.pdf',
  preferCSSPageSize: true,
  printBackground: true,
  scale: 1
});

Use one source of truth for page size. Mixing a CSS @page size with an unrelated format can produce unexpected fitting or clipping.

4. Screen CSS versus print CSS

The PDF method generates with the print media type by default. Print styles often hide navigation, change colors, remove fixed elements, or rearrange columns. That is useful for a document but surprising when you want a screenshot-like PDF.

await page.emulateMediaType('screen'); // screen styles
await page.pdf({path: 'screen-styled.pdf', format: 'A4'});

To use the site’s print design, omit the call or explicitly select print:

await page.emulateMediaType('print');
await page.pdf({path: 'print-styled.pdf', format: 'A4'});

The emulateMediaType() method changes the active CSS media type; it does not set paper dimensions.

5. How to tune scale without guessing

Start with scale: 1. First verify media type, viewport dimensions, page size, margins, and preferCSSPageSize. Only then adjust PDF scale. Lower values fit more content onto a page; higher values enlarge rendered content and can increase clipping or pagination.

  1. Capture with the intended viewport and scale: 1.
  2. Check whether the wrong CSS media type is active.
  3. Check the PDF paper dimensions and margins.
  4. Check whether a CSS @page rule is overriding your options.
  5. Adjust scale in small increments, such as 0.9 or 1.1.
  6. Compare several representative pages, including long text, wide tables, images, and lazy-loaded content.

There is no documented universal numeric conversion between device pixel ratio and PDF scale. A deviceScaleFactor of 3 does not imply scale: 3; PDF scale is limited to 0.1–2.

6. Mobile layout details that affect the result

Meta viewport

Responsive pages commonly include <meta name='viewport' content='width=device-width, initial-scale=1'>. Without an appropriate meta viewport, a mobile emulation may render a desktop-style layout inside a wide layout viewport. If you control the page, verify this tag. If you do not, inspect the computed layout before changing PDF settings.

networkidle2 waits for a low number of active connections, but it cannot guarantee that application data or lazy images are ready. Wait for a meaningful selector or application signal when possible.

await page.goto(target, {waitUntil: 'domcontentloaded', timeout: 60000});
await page.waitForSelector('[data-report-ready]', {timeout: 30000});
await page.evaluate(() => document.fonts.ready);

Lazy images and long pages

A viewport-sized PDF may not trigger lazy content below the fold. For full documents, scroll in steps before printing, or use an application-specific ready signal. Keep in mind that increasing the viewport height changes layout and can change which responsive breakpoint is active.

Fixed and sticky elements

Headers fixed to the viewport can repeat visually or overlap content in print output. Add print CSS to change them to static positioning, or hide them for the PDF. This is a CSS issue, not a deviceScaleFactor issue.

7. Troubleshooting checklist

Symptom Likely cause Fix
PDF looks desktop-sized Emulation happened after navigation, or the page lacks a mobile meta viewport Set viewport or call page.emulate() before goto; inspect the meta viewport.
Colors and layout differ from the browser PDF is using print CSS Call await page.emulateMediaType('screen') before page.pdf().
Content is shrunk unexpectedly Paper size, margins, or CSS @page causes fitting Set explicit dimensions, review margins, and use preferCSSPageSize: true when CSS owns sizing.
Changing DPR has no paper-size effect deviceScaleFactor is a viewport property Change PDF width, height, or format.
Right edge is clipped Viewport content is wider than the PDF page or scale is too large Match page width to the layout, remove margins, or lower PDF scale.
Fonts are missing PDF started before web fonts loaded Await document.fonts.ready and ensure font requests succeed.
Images are blank Lazy loading or failed resource requests Wait for an image-ready selector, scroll to trigger loading, and inspect network failures.
Mobile breakpoint is wrong Viewport width is not the intended CSS width Set width in CSS pixels; do not derive it from DPR.
PDF times out Long-running requests, blocked scripts, or an application that never becomes idle Use a suitable wait condition and explicit selector timeout; avoid waiting forever for network idle.

8. Performance, reliability, and cost considerations

Launching Chromium is expensive compared with reusing a browser process. For batch jobs, keep one browser alive and create isolated pages or contexts per capture. Close pages in a finally block so failed jobs do not accumulate resources.

Choose the smallest viewport and paper that satisfy the output. Large full-page PDFs consume more memory, especially with high-resolution images. A higher device scale factor can increase page rendering work, but it remains independent from PDF paper size. Measure your own workload across the Chromium and Puppeteer versions you deploy; the documentation does not provide a universal throughput or scale benchmark.

For reliability, pin Puppeteer and its Chromium revision, log the URL, viewport, media type, paper settings, and timing, and retain a small set of golden PDFs for visual comparison after upgrades. Treat navigation, selector waits, font readiness, and PDF generation as separate timeout stages so failures identify the broken stage.

Self-hosted Puppeteer has infrastructure costs for Chromium processes, memory, storage, and retries. A managed API can exchange browser maintenance for per-capture pricing. Review the provider’s billing rules for failed pages, caching, and asynchronous jobs before estimating cost.

9. Or skip the browser setup

ScreenshotNeo provides a single GET request for a PNG, JPEG, WebP, or PDF. It supports viewport and device presets, full-page capture, PDF paper size, margins, landscape mode, page ranges, custom CSS and JavaScript, selector waits, network-idle waits, headers, cookies, user agents, geolocation, caching, asynchronous jobs, bulk capture, and an OpenAPI specification. See the ScreenshotNeo documentation for the option names and request details.

A capture pipeline can remove consent UI and overlays before rendering the final file.
A capture pipeline can remove consent UI and overlays before rendering the final file.

The same service can handle the cleanup work around real pages: it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

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 failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo includes 1,000 shots per month free with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account and try the request with your own URL.

10. A practical validation plan

  1. Select one phone, one tablet, and one desktop profile that represent your users.
  2. Record CSS viewport width and height, device scale factor, media type, paper dimensions, margins, and PDF scale.
  3. Capture a page with responsive navigation, long text, images, forms, and a table.
  4. Compare the PDF against a browser rendering at the same CSS viewport.
  5. Repeat after changing only one variable so you can identify the control responsible for a difference.
  6. Save the resulting settings beside your code and rerun the comparison when upgrading Puppeteer or Chromium.

FAQ

Does deviceScaleFactor determine PDF DPI?

No. It changes the emulated device pixel ratio. PDF paper dimensions and PDF rendering scale are separate options.

Why does my PDF use print styles?

page.pdf() uses print CSS by default. Call page.emulateMediaType('screen') before generating the PDF for screen styles.

Should I set both format and width/height?

Usually choose one approach. When format is set, it takes priority over explicit width and height.

When should preferCSSPageSize be true?

Use it when the page’s CSS @page rule is the authoritative paper definition. Leave it false when Puppeteer’s paper options should control fitting.

Can I calculate a universal scale from a phone’s DPR?

No. Layout, CSS page rules, margins, fonts, and Chromium version affect the result. Validate the output for your pages.