ScreenshotNeo

BlogHow-to

Puppeteer: Set Screenshot Output Dimensions

Set Puppeteer screenshot dimensions precisely with viewport, clip, fullPage, and device scale settings, plus runnable examples and troubleshooting.

By the ScreenshotNeo team1 October 20268 min read

Use page.setViewport() to control the page’s layout viewport, then choose clip or fullPage to control what gets captured. These settings solve different problems:

  • width and height describe the emulated viewport in CSS pixels.
  • deviceScaleFactor controls device scaling and defaults to 1.
  • clip selects a screenshot rectangle.
  • fullPage: true captures the entire document extent.

The complete example below sets a 1,200 by 800 CSS-pixel viewport, captures that viewport, captures a selected rectangle, and captures the full page.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.setViewport({
  width: 1200,
  height: 800,
  deviceScaleFactor: 1,
});

await page.goto('https://example.com', { waitUntil: 'networkidle2' });

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

await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 1200, height: 800 },
});

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

await browser.close();

Puppeteer’s official documentation defines viewport dimensions as CSS pixels and documents deviceScaleFactor as the device scale setting, with a default of 1. See the Viewport interface and ScreenshotOptions interface.

How Puppeteer dimensions work

A screenshot’s apparent size can involve three separate coordinate systems:

Goal API What it changes
Set page layout and responsive breakpoints page.setViewport({ width, height }) The emulated viewport, measured in CSS pixels
Capture one rectangle page.screenshot({ clip }) The output bounds selected from the page
Capture the entire document page.screenshot({ fullPage: true }) The capture extent, beyond the visible viewport
Resize the browser content area page.resize({ contentWidth, contentHeight }) The browser’s content area; the API is experimental

A viewport of 1200 by 800 does not mean every output file will have exactly 1200 by 800 pixels. Device scale, browser version, clipping behavior, and full-page layout can affect raster dimensions. When exact file dimensions matter, inspect the generated image in your installed Puppeteer and Chromium environment.

Set the viewport dimensions

await page.setViewport({
  width: 1440,
  height: 900,
  deviceScaleFactor: 1,
});

width and height are CSS-pixel dimensions. They affect responsive CSS, media queries, layout, and the visible page area. The viewport configuration should be applied before navigation when possible.

Retina-style output

await page.setViewport({
  width: 1200,
  height: 800,
  deviceScaleFactor: 2,
});
await page.goto('https://example.com');
await page.screenshot({ path: 'retina.png' });

A larger device scale can produce a denser raster while keeping the page’s CSS layout at 1200 by 800. Do not assume a universal CSS-to-file-pixel formula across all combinations; verify the file produced by your Puppeteer and Chromium versions.

Mobile and touch emulation

await page.setViewport({
  width: 390,
  height: 844,
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
});

Changing mobile or touch emulation can reload a page in some cases. Set the viewport before navigation, or wait for the page to finish reloading before capturing.

Capture a fixed rectangle with clip

await page.screenshot({
  path: 'header.png',
  clip: {
    x: 0,
    y: 0,
    width: 1200,
    height: 180,
  },
});

x and y identify the rectangle’s origin. width and height identify its dimensions. Use clipping when you need a predictable region such as a header, chart, card, or above-the-fold area.

Capture an element’s bounding box

const chart = await page.locator('#chart').boundingBox();
if (!chart) throw new Error('Chart is not visible');

await page.screenshot({
  path: 'chart.png',
  clip: chart,
});

Wait for the element to exist and be visible before reading its box. A hidden element can return no bounding box, and a layout shift after measurement can make the result inaccurate.

Capture outside the current viewport

Puppeteer’s captureBeyondViewport option controls whether a clipped area outside the visible viewport may be captured. The documented default is false when no clip is supplied and true when a clip is supplied. Set it explicitly when your code depends on this behavior:

await page.screenshot({
  path: 'lower-region.png',
  clip: { x: 0, y: 1200, width: 1200, height: 500 },
  captureBeyondViewport: true,
});

Capture the full page

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

fullPage: true requests the full page extent; it does not change the emulated viewport. Responsive layout still uses the viewport configured with setViewport(). Puppeteer’s documentation describes fullPage as taking a screenshot of the full page, and its default is false.

Prepare lazy-loaded content

Full-page captures can miss content that only loads after scrolling or interaction. A simple scroll pass can trigger many lazy-loading implementations:

await page.evaluate(async () => {
  await new Promise((resolve) => {
    let y = 0;
    const step = 600;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 100);
  });
});

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

The exact lazy-loading trigger is application-specific. If the site exposes a reliable readiness selector, wait for that selector instead of relying only on a delay.

Resize the browser content area

If you need to change the browser’s content area itself rather than page emulation, Puppeteer documents Page.resize with contentWidth and contentHeight. The current Page API labels this method experimental:

await page.resize({
  contentWidth: 1200,
  contentHeight: 800,
});

Prefer setViewport() for normal screenshot automation because it is the usual control for responsive layout. Use content-area resizing only when your browser-management requirement specifically calls for it.

Complete reusable helper

import puppeteer from 'puppeteer';

async function capture({
  url,
  output,
  width = 1200,
  height = 800,
  deviceScaleFactor = 1,
  fullPage = false,
  clip,
}) {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width, height, deviceScaleFactor });
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
    await page.screenshot({
      path: output,
      fullPage,
      ...(clip ? { clip } : {}),
    });
  } finally {
    await browser.close();
  }
}

await capture({
  url: 'https://example.com',
  output: 'example.png',
  width: 1366,
  height: 768,
  deviceScaleFactor: 1,
});

Page.screenshot() can return image data instead of writing a file. The API offers a base64-string overload and a Uint8Array result; use the returned bytes when sending the image to object storage or another service.

Common mistakes and edge cases

  • Confusing viewport size with output size: the viewport controls layout; clip controls a selected capture rectangle; fullPage controls document extent.
  • Measuring before fonts or images load: wait for a meaningful selector, font readiness, or a stable network state before calculating a bounding box.
  • Capturing an invisible element: boundingBox() can return null. Check visibility and scroll the element into view.
  • Unexpected horizontal cropping: inspect the page for fixed-width content or horizontal overflow. A full-page screenshot follows the document layout, including overflow behavior.
  • Viewport changes causing a reload: set mobile and touch options before navigation and wait for the resulting page load.
  • Very tall pages: large full-page images consume significant memory. Capture selected regions or split the document when downstream systems have size limits.
  • Different dimensions after upgrading Puppeteer: compare the installed Puppeteer and Chromium versions and inspect the output file. Puppeteer’s v7.0.0 changelog records a clip behavior change: screenshots use clip dimensions instead of cutting them by the viewport.

Troubleshooting

Symptom Likely cause Fix
The image is only the viewport, not the whole page fullPage is omitted or false Set fullPage: true; keep the desired responsive viewport in setViewport().
The clipped image is cut off Clip coordinates or dimensions do not match the page layout, or capture beyond the viewport is disabled Recalculate the box after layout settles and set captureBeyondViewport: true when needed.
The element screenshot is blank The element is hidden, detached, or not yet rendered Wait for the selector, verify its bounding box, and capture after rendering completes.
Text or images move between runs Fonts, animations, ads, or late network requests are still changing layout Wait for a stable readiness condition, disable animations where appropriate, and use a deterministic test page state.
Mobile layout is not applied The viewport was changed after navigation or mobile emulation was not enabled Set isMobile, hasTouch, and dimensions before goto(), then wait for navigation.
Capture times out The page or a resource never becomes ready Set a deliberate navigation timeout, use a less strict waitUntil condition, and wait for the specific content required by the screenshot.

Performance and reliability

  • Reuse a browser process for batches of screenshots, while creating isolated pages for independent jobs.
  • Use a viewport capture when you only need the visible area. Full-page captures require more layout work and produce larger files.
  • Choose the lowest deviceScaleFactor that meets your visual quality requirement; higher density increases raster size and memory use.
  • Wait for a page-specific readiness signal instead of an arbitrary long delay. This reduces wasted time on fast pages and avoids racing slow components.
  • For repeatable output, control viewport, timezone, locale, fonts, animations, network conditions, and authenticated state.
  • Record the Puppeteer and Chromium versions with generated artifacts. Screenshot behavior can change across releases.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns a PNG, JPEG, WebP, or PDF. The API supports full-page capture, CSS-selector element capture, custom viewports, 12 device presets, retina scale, waits, custom CSS and JavaScript, hidden selectors, request blocking, cookies, headers, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and PDF options.

See the ScreenshotNeo API documentation for the available parameters. This cURL example captures Stripe:

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

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also has an MCP server so Claude, Cursor, and other MCP clients can take screenshots. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

Cost considerations

Running Puppeteer yourself means paying for the compute, browser memory, storage, and operational work required to keep Chromium jobs reliable. Keep files small when possible, avoid unnecessary full-page or high-scale captures, and cache identical results in your own system.

With ScreenshotNeo, only clean shots are billed. Its plans are Free (1,000 shots/month), 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, and every feature is available on every plan.

FAQ

Does setViewport() resize the browser window?

It sets the page’s emulated viewport. Browser content-area resizing is a separate, experimental page.resize() API.

Should I use clip or fullPage?

Use clip for a known rectangle or element. Use fullPage: true when the entire document is required.

What is the default device scale?

Puppeteer’s documented default for deviceScaleFactor is 1.

Can I guarantee exact image pixels from CSS dimensions?

CSS viewport dimensions and raster output are related but can differ with device scale and browser behavior. Inspect the generated file when exact pixel dimensions are a requirement.

Can Puppeteer return screenshot bytes instead of a file?

Yes. Page.screenshot() provides overloads for base64 output and a Uint8Array result.

Official references