How to Capture HTML Pages as Screenshots with Playwright
Capture viewport, full-page, and element screenshots with Playwright, choose formats and scale, stabilize output, and troubleshoot common failures.
Playwright captures an HTML page with page.screenshot(). The default is the visible viewport; add fullPage: true for the entire scrollable document. Use locator.screenshot() when you need one element.
The basic flow is: launch a browser, create a page, navigate, capture, then close the browser.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png', fullPage: true });
await browser.close();
})();
Install Playwright and its browsers first:
npm install playwright
npx playwright install
See the Page API and screenshots guide for version-specific details.
1. Capture a viewport screenshot
A normal page screenshot records the current viewport. Set the viewport before navigation when a predictable output size matters.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'viewport.png', type: 'png' });
await browser.close();
})();
path saves the image. Without it, Playwright returns a buffer, which is useful for an HTTP response or object storage.
const image = await page.screenshot({ type: 'webp', quality: 85 });
require('fs').writeFileSync('viewport.webp', image);
2. Capture the full HTML page
Use fullPage: true to capture the complete scrollable document instead of only what is visible. Playwright lays out the page as one tall image.
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
Full-page capture can be very tall. Long pages may produce large files or expose layout problems in sticky headers, fixed elements, canvas content, and lazy-loaded sections. If the page loads content while scrolling, make the page reveal it before the screenshot:
await page.goto('https://example.com');
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: 'loaded-full-page.png', fullPage: true });
3. Capture one element
Use a locator when the output should contain a card, header, chart, or other component.
const header = page.locator('header');
await header.screenshot({ path: 'header.png' });
Playwright waits for locator actionability and scrolls the element into view. A scrollable container is captured at its current scroll position, so it does not automatically include all of that container’s hidden content.
const chart = page.locator('[data-testid="sales-chart"]');
await chart.waitFor({ state: 'visible' });
await chart.screenshot({ path: 'sales-chart.webp', type: 'webp', quality: 90 });
4. Choose PNG, JPEG, or WebP
| Format | Best for | Options |
|---|---|---|
| PNG | Lossless UI, text, transparency | Default; quality is not used |
| JPEG | Photographic pages and smaller files | quality defaults to 80; no transparency |
| WebP | Small modern web images | Quality defaults to 100; lower values are lossy |
await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 80 });
await page.screenshot({ path: 'page.webp', type: 'webp', quality: 85 });
When a path is supplied, Playwright can infer the type from the filename extension. Specify type when the output format must be explicit.
5. Control resolution with scale
scale: 'css' produces one image pixel per CSS pixel. scale: 'device' uses device pixels and can make high-DPI images twice as large or larger.
await page.screenshot({ path: 'compact.png', scale: 'css' });
await page.screenshot({ path: 'retina.png', scale: 'device' });
Use CSS scale for predictable dimensions and smaller files. Use device scale when the image is intended for a high-DPI display.
6. Make screenshots repeatable
Animations, blinking carets, clocks, random data, ads, and late network requests can change pixels between runs. Playwright provides screenshot controls for common sources of drift.
await page.screenshot({
path: 'stable.png',
animations: 'disabled',
caret: 'hide',
mask: [page.locator('.user-name'), page.locator('.live-counter')],
maskColor: '#FF00FF',
style: `* { transition: none !important; animation: none !important; }`
});
Use clip to capture a rectangle and omitBackground: true for transparency. Transparent output is not available for JPEG.
await page.screenshot({
path: 'region.png',
clip: { x: 40, y: 80, width: 800, height: 500 },
omitBackground: true
});
Wait for a meaningful page state instead of relying only on a fixed delay:
await page.goto('https://example.com');
await page.locator('main').waitFor({ state: 'visible' });
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'ready.png' });
7. Use screenshot assertions for visual tests
With the Playwright test runner, expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparing the result with the stored expectation.
import { test, expect } from '@playwright/test';
test('homepage stays visually stable', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled'
});
});
Screenshot assertions only work with the Playwright test runner. Keep the browser version, viewport, fonts, locale, timezone, and test data consistent across environments.
8. Configure browser context and page state
Create a context when you need a device profile, locale, timezone, color scheme, or authenticated session.
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 1,
colorScheme: 'dark',
locale: 'en-US',
timezoneId: 'America/New_York',
userAgent: 'ScreenshotWorker/1.0'
});
const page = await context.newPage();
For authenticated pages, load saved storage state or set cookies before navigation. Treat stored state and custom headers as secrets.
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
await context.addCookies([{ name: 'session', value: process.env.SESSION, domain: 'example.com', path: '/' }]);
9. Complete Node.js example
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
colorScheme: 'light'
});
const page = await context.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.locator('body').waitFor({ state: 'visible' });
await page.screenshot({
path: 'example.webp',
fullPage: true,
type: 'webp',
quality: 90,
scale: 'css',
animations: 'disabled',
caret: 'hide'
});
} finally {
await browser.close();
}
})();
10. Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Browser executable missing | Playwright package is installed without browser binaries | Run npx playwright install or install the required browser explicitly. |
| Timeout during navigation | Slow server, blocked request, or a page that never becomes idle | Set a suitable timeout, use domcontentloaded, and wait for a specific selector instead of indefinite network idle. |
| Blank or partial screenshot | Capture happened before the app rendered | Wait for the main locator, a response, or an application-ready marker. |
| Lazy images are missing | Images load only after entering the viewport | Scroll through the page or trigger the site’s lazy-load mechanism before full-page capture. |
| Element is not visible | Selector matches a hidden, covered, or detached element | Use a precise locator, wait for visibility, and inspect overlays or frames. |
| Different pixels on every run | Animations, caret, time, ads, random data, or fonts | Disable animations, hide the caret, mask dynamic regions, freeze test data, and install identical fonts. |
| Full page has unexpected width | Responsive layout changed at the chosen viewport | Set the context viewport explicitly and verify mobile or desktop breakpoints. |
| Screenshot of iframe content is empty | Selector targets the iframe element rather than its document | Use frameLocator() and locate content inside the frame. |
| JPEG has a black or unexpected background | Transparency is unsupported in JPEG | Use PNG or WebP when transparency is required. |
11. Performance, reliability, and cost
- Reuse a browser process for multiple pages, but create isolated contexts for separate cookies and sessions.
- Viewport screenshots are generally cheaper in time and memory than very tall full-page images.
- Use CSS scale and WebP or JPEG when file size matters; use PNG and device scale when pixel fidelity matters.
- Set navigation and action timeouts, close contexts in a
finallyblock, and retry only transient failures. - Pin Playwright and browser versions for visual regression. A browser update can change fonts, layout, and antialiasing.
- Cache stable assets where appropriate, but do not cache personalized pages across users.
- Self-hosted Playwright costs the compute, browser, bandwidth, and storage used by each capture. Full-page pages consume more memory and transfer bandwidth.
12. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off.
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)
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}`);
Read the ScreenshotNeo API documentation for the full option list. You can request full pages, an element by CSS selector, dark mode, device presets or custom viewports, retina scale, PDF paper sizes and page ranges, custom CSS and JavaScript, clicks, selector or network waits, blocked ads and trackers, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching with a chosen TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.
Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers identify the page verdict and whether the shot was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
13. FAQ
What is the simplest Playwright screenshot command?
await page.screenshot({ path: 'screenshot.png' }) captures the current viewport.
How do I take a full-page screenshot?
Pass fullPage: true: await page.screenshot({ path: 'page.png', fullPage: true }).
Can Playwright screenshot a single CSS selector?
Yes. Use await page.locator('.selector').screenshot({ path: 'element.png' }).
Which format should I choose?
Use PNG for lossless text and transparency, JPEG for photographic pages and smaller files, and WebP for compact modern output.
Why do visual tests fail even when the page looks the same?
Dynamic pixels, fonts, browser versions, viewport settings, animations, and timing can differ. Disable animations, mask changing regions, and keep the environment fixed.


