Puppeteer: Set Screenshot Output Dimensions
Set Puppeteer screenshot dimensions precisely with viewport, clip, fullPage, and device scale settings, plus runnable examples and troubleshooting.
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:
widthandheightdescribe the emulated viewport in CSS pixels.deviceScaleFactorcontrols device scaling and defaults to1.clipselects a screenshot rectangle.fullPage: truecaptures 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;
clipcontrols a selected capture rectangle;fullPagecontrols 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 returnnull. 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
deviceScaleFactorthat 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.


