How to take screenshots with Playwright in Node.js

How to take screenshots with Playwright in Node.js

The definitive tutorial — from first install to production-grade captures.

March 28, 2026 · 10 min read

Playwright is Microsoft's open-source browser automation library and it's become the de facto standard for headless browser work in Node.js. If you need to programmatically capture screenshots of web pages, Playwright gives you the most control with the least friction. This tutorial covers everything from installation to advanced capture techniques.

Installation

Playwright ships as an npm package with its own managed browser binaries. This means you don't need to install Chrome separately — Playwright downloads exactly the browser version it's tested against.

Terminal
npm init -y
npm install playwright

Then install the browser binaries:

npx playwright install chromium

You can also install firefox or webkit, but Chromium is what you want for screenshots — it matches what most users see in Chrome and Edge.

Basic screenshot

The simplest possible screenshot — launch a browser, navigate to a page, capture it:

JavaScript
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
await browser.close();

This captures the visible viewport (default 1280×720) as a PNG file. The browser runs headless — no visible window opens.

Setting the viewport

The viewport determines the "window size" for the screenshot. Set it when creating a new page:

const page = await browser.newPage({
  viewport: { width: 1440, height: 900 }
});

Common viewport sizes: 1440×900 for desktop, 1920×1080 for full HD, 390×844 for iPhone 14, 768×1024 for iPad.

Full-page screenshots

By default, Playwright captures only the visible viewport. To capture the entire scrollable page:

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

Full-page captures stitch together the entire scroll height into a single image. Be aware: long pages produce large files. A page that scrolls for 10,000 pixels vertically at 2x DPR produces a 2880×20000 pixel image. There's a dedicated full-page screenshots guide that covers lazy loading, infinite scroll, and fixed headers.

Lazy loading gotcha: Images below the fold won't load until scrolled into view. Before a full-page capture, scroll the page to trigger lazy loading, then scroll back to top. Playwright doesn't do this automatically.

Element screenshots

Capture a specific DOM element instead of the full page:

const element = await page.locator('.hero-section');
await element.screenshot({ path: 'hero.png' });

This crops the output to the element's bounding box. Useful for capturing specific components — a pricing table, a chart, a form.

Device pixel ratio (Retina)

Modern displays render at 2x or 3x resolution. Set deviceScaleFactor to match:

const page = await browser.newPage({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 2
});

At 2x DPR, a 1440×900 viewport produces a 2880×1800 pixel image. Higher quality, but larger file size.

Waiting for the page to load

This is where most screenshot automation breaks. Pages aren't "done" when the HTML loads — JavaScript needs to execute, fonts need to render, images need to download. Playwright offers several wait strategies:

// Wait until network is idle (no requests for 500ms)
await page.goto(url, { waitUntil: 'networkidle' });

// Wait for a specific element to appear
await page.waitForSelector('.content-loaded');

// Wait for a specific amount of time after load
await page.goto(url);
await page.waitForTimeout(2000);

// Wait for fonts to load
await page.evaluate(() => document.fonts.ready);

For most pages, networkidle works well. For SPAs that hydrate slowly, combine it with waitForSelector targeting an element that only appears when the app is ready. The waitForTimeout approach is a blunt instrument but sometimes necessary as a safety buffer.

Dark mode emulation

Capture how a page looks in dark mode without changing system settings:

const page = await browser.newPage({
  colorScheme: 'dark'
});

This sets the prefers-color-scheme media query to dark, so any page with dark mode CSS will render in its dark variant.

Output formats

Playwright supports PNG and JPEG:

// PNG (default) — lossless, larger files
await page.screenshot({ path: 'out.png' });

// JPEG — lossy, smaller files
await page.screenshot({
  path: 'out.jpg',
  type: 'jpeg',
  quality: 85
});

PNG is better for screenshots of text-heavy pages (sharp edges). JPEG is better when file size matters and the content is image-heavy.

Clip regions

Capture a specific rectangle of the page:

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

The coordinates are in CSS pixels (before DPR scaling). Useful for capturing above-the-fold content or specific regions without needing element selectors.

Handling cookie banners and pop-ups

Cookie consent banners will appear in your screenshots unless dismissed. Two approaches:

// Approach 1: Click the accept button
try {
  await page.click('[data-testid="cookie-accept"]', { timeout: 3000 });
} catch (e) {
  // No cookie banner, continue
}

// Approach 2: Inject CSS to hide common banner selectors
await page.addStyleTag({
  content: `
    [class*="cookie"], [id*="cookie"],
    [class*="consent"], [id*="consent"],
    .cc-banner, #onetrust-banner-sdk { display: none !important; }
  `
});

Neither approach is foolproof across all sites. CSS injection is more reliable because it doesn't depend on specific button selectors.

Browser context for multiple pages

If you're taking many screenshots, reuse the browser and create new pages (not new browsers) for each capture:

const browser = await chromium.launch();

// Take many screenshots efficiently
for (const url of urls) {
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  await page.goto(url, { waitUntil: 'networkidle' });
  await page.screenshot({ path: `screenshots/${slugify(url)}.png` });
  await page.close(); // Close the page, keep the browser
}

await browser.close();

Creating a new page is fast (~50ms). Launching a new browser is slow (~500ms). Reuse the browser.

When to use an API instead

Running Playwright yourself means managing Chromium binaries, handling crashes, dealing with memory leaks, and keeping the browser updated. If you need reliable screenshots without the operational overhead, a screenshot API handles all of this for you. This becomes especially clear when you try to run Playwright in a serverless environment — it doesn't work.

With nightglass, the same screenshot is one HTTP request:

curl -X POST https://api.nightglass.xyz/api/v1/screenshot \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"url": "https://example.com", "width": 1440, "height": 900}' \
  --output screenshot.png

Half a cent per screenshot, no browser to manage. Playwright also has excellent built-in visual regression testing — if that's your use case, that guide covers the full workflow. For a comparison of options, see Puppeteer vs Playwright.