How to Capture a Webpage Screenshot at a Specific Viewport Size
Set exact browser viewport dimensions with Playwright or Puppeteer, understand CSS and device pixels, and troubleshoot common screenshot size issues.
To capture a webpage at an exact viewport size, set the browser viewport width and height in CSS pixels before navigating to the page, then take a regular viewport screenshot. For example, a 1440 × 900 viewport is set with width: 1440 and height: 900. A viewport screenshot captures the visible browser page area; a full-page screenshot instead includes content below the fold.
These three settings are related but distinct: viewport size controls the page’s CSS layout, device emulation can also set properties such as user agent and touch behavior, and output scale controls whether the image is measured in CSS pixels or device pixels. For most desktop captures, an explicit viewport is all you need.
1. Capture an exact viewport with Playwright
Install Playwright and its browser, then save this as screenshot.js. Replace the dimensions and URL with your target values.
npm install playwright
npx playwright install chromium
// screenshot.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'screenshot.png' });
} finally {
await browser.close();
}
})();
Run it with node screenshot.js. The call to setViewportSize() resizes the page and resets its screen size. Set the viewport before navigation when the page’s mobile or responsive layout should be chosen on initial load; some sites do not adapt as expected if you change to phone dimensions after loading.
If you need several pages or tests to share the same viewport, set it when creating a context:
const context = await browser.newContext({
viewport: { width: 1440, height: 900 }
});
const page = await context.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
2. Choose the right capture dimensions
Viewport capture versus full-page capture
A normal Playwright screenshot captures the visible viewport. For the example above, its logical dimensions are 1440 × 900 CSS pixels. To capture the entire scrollable page instead, use fullPage: true:
await page.screenshot({ path: 'full-page.png', fullPage: true });
A full-page image can be much taller than the viewport, so it does not serve the same purpose as an exact-size viewport capture. Use it when you need below-the-fold content, not when the output must represent what fits inside a particular browser window.
CSS pixels and device pixels
Viewport width and height are specified in CSS pixels. The screenshot’s scale option can produce output scaled to CSS pixels or device pixels. A device-pixel image can have more physical pixels than the CSS viewport, especially with a device scale factor above 1. If downstream code requires a particular image-file width and height, check the resulting image dimensions rather than assuming CSS pixels and image pixels are interchangeable.
Keep the device scale factor at its default for a straightforward viewport-sized image. When reproducing a high-density display, configure a device scale factor and select device-pixel output. This increases image dimensions and file size; it does not change the page’s CSS layout width.
Viewport versus device emulation
Setting only width and height reproduces a viewport size. It does not, by itself, reproduce a specific phone. Device presets can also configure user agent, touch support, and device scale factor. If using a preset with custom dimensions, spread the preset first and then set your explicit viewport so it overrides the preset viewport:
const { chromium, devices } = require('playwright');
const browser = await chromium.launch();
const phone = devices['iPhone 13'];
const context = await browser.newContext({
...phone,
viewport: { width: 390, height: 844 }
});
const page = await context.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'mobile.png' });
await browser.close();
Use the desired preset available in your Playwright version. If exact reproduction matters, record the viewport, device scale factor, user agent, browser version, and relevant context settings alongside the screenshot.
3. Puppeteer equivalent
Puppeteer uses page.setViewport(). This complete Node.js script sets the viewport before navigation and writes a viewport screenshot:
npm install puppeteer
// puppeteer-screenshot.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshot.png' });
} finally {
await browser.close();
}
})();
Run with node puppeteer-screenshot.js. For device emulation, Puppeteer provides device descriptors through its supported device APIs; configure the desired device before navigation. As with Playwright, a viewport-only setting does not automatically reproduce every property of a physical device.
4. Make captures repeatable
- Set dimensions before navigation. This lets responsive breakpoints and scripts initialize against the intended viewport.
- Choose a readiness condition. Use a navigation wait condition that suits the page.
networkidlecan be useful for mostly static pages, but analytics, streaming, polling, or long-lived connections can prevent network idle from occurring. In those cases, wait for a specific selector or a bounded delay after navigation. - Wait for content that matters. A page can finish navigation before client-rendered content or images appear. Wait for a relevant element when the capture depends on it.
- Keep the capture mode explicit. Use the default viewport screenshot for visible-area output; opt into full-page capture only when needed.
- Check the produced file. Verify its pixel dimensions and format in your own pipeline, especially when using device scale factors or image processing.
Example of waiting for an element rather than relying only on network activity:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('main article').waitFor({ state: 'visible' });
await page.screenshot({ path: 'article.png' });
5. Common problems and fixes
| Problem | Likely cause | Fix |
|---|---|---|
| The layout uses the wrong responsive breakpoint | The viewport was changed after the page loaded, or only screen size was set. | Configure the viewport before navigation. If you need separate screen and viewport values, use a browser context with both configured. |
| The image file is larger than the requested dimensions | The output uses device pixels, or a device scale factor greater than 1. | Use CSS-pixel screenshot scaling for CSS-sized output, or account for the device scale factor in expected image dimensions. |
| The screenshot is only as tall as the window | A viewport screenshot captures the visible page area. | Use full-page capture if you need the entire scrollable page. Expect a taller image. |
| Some content is missing or still loading | Navigation completed before client rendering, lazy loading, or image loading finished. | Wait for a content selector or the relevant resource state. For lazy content, scroll it into view or use the browser tool’s full-page behavior as appropriate. |
| The script hangs waiting for network idle | The site has polling, analytics, streaming, or other ongoing requests. | Use a less restrictive navigation wait condition, then wait for a specific element or a bounded delay. |
| Browser launch fails in a fresh environment | The automation package is installed but its browser executable or system dependencies are missing. | Install the browser for the automation package using its documented install command, and check the environment’s browser dependencies. |
| Mobile page still looks like desktop | Only dimensions were changed, while the site or scripts also depend on device properties. | Use a device preset or configure user agent, touch behavior, and device scale factor as required; set these before navigation. |
6. Performance, reliability, and cost
Browser automation gives you direct control over browser context and page behavior, but each capture requires a browser process or an existing browser session, page navigation, resource loading, and image encoding. Reuse a browser process across captures when appropriate, while creating isolated contexts when pages need separate cookies, settings, or viewport dimensions. Always close pages, contexts, and browsers so repeated jobs do not accumulate resources.
For reliability, bound navigation and element waits with timeouts, handle navigation failures, and capture diagnostic information when a page is blank or incomplete. A fixed delay is easy to understand but may waste time on fast pages and still be too short on slow ones; a meaningful selector is usually a more targeted readiness signal. Exact output can also vary when page content, fonts, animations, network responses, or browser versions change.
Self-hosted browser automation has no per-screenshot API fee, but it uses compute, memory, bandwidth, and engineering time. Hosted screenshot APIs trade some browser setup and maintenance for a service charge; compare billing rules as well as headline quotas, since failed or blank captures may be treated differently by different providers.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Set the requested viewport with its documented parameters; the following one-call example captures a page, and the ScreenshotNeo documentation describes the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);
To request a specific viewport, add the width and height parameters documented by ScreenshotNeo to the request. Cookie banners, popups, and chat widgets are removed 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, and paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does viewport width include browser tabs and toolbars?
No. Automation viewport dimensions describe the page area, not the outer desktop window or browser chrome.
Should I use a viewport screenshot or a full-page screenshot for a fixed-size preview?
Use a viewport screenshot. A full-page capture changes the output height to include the scrollable document.
Will the same dimensions always produce identical pixels?
No. The viewport fixes the page’s layout area, but dynamic content, fonts, animation, browser version, and device scale settings can affect the rendered image.


