The definitive tutorial — from first install to production-grade captures.
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.
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.
Terminalnpm 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.
The simplest possible screenshot — launch a browser, navigate to a page, capture it:
JavaScriptimport { 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.