Appearance
Playwright Screenshots
Capture retina-quality screenshots with Playwright — custom viewports, device scale factors, and tooltip states.
When to use
Use these patterns when capturing polished screenshots of a web page for documentation, design review, or visual storytelling. Particularly useful for prototypes where you need to show hover states, tooltips, or multiple UI configurations in a single batch.
The pattern
Retina screenshots with a dedicated context
Create a separate browser context with a specific viewport and device scale factor. A scale factor of 2 produces retina-quality output at double the CSS pixel dimensions:
javascript
import { chromium } from "playwright";
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 1024 },
deviceScaleFactor: 2,
});
const page = await context.newPage();
await page.goto("http://localhost:8080");
await page.waitForLoadState("networkidle");Take screenshots at device scale so the output reflects the scale factor. A 1440×1024 viewport at 2x produces a 2880×2048 PNG:
javascript
await page.screenshot({
path: "screenshots/01-default.png",
scale: "device",
});Use zero-padded filenames (01-, 02-) to keep screenshots in sequence order.
Adding a cursor overlay
Real mouse cursors don't appear in Playwright screenshots. See Playwright Mouse Cursor for the showCursor/hideCursor helpers and setup. The examples below assume those helpers are available.
Capturing tooltip and hover states
Tooltips require a specific interaction sequence — click the target first to select it, move the mouse away, then hover back to trigger the tooltip. For example, to capture a tooltip on a button:
javascript
const button = page.locator("button.my-button");
// Click to select, move away, hover back to trigger tooltip
await button.click();
await page.mouse.move(0, 0);
await button.hover();
await page.waitForSelector('[role="tooltip"]', {
state: "visible",
});
// Position pointer cursor centered on the button
const box = await button.boundingBox();
await showCursor(page, {
src: pointerUri,
x: box.x + box.width / 2 - 16,
y: box.y + box.height / 2 - 16,
width: 32,
height: 32,
});
await page.screenshot({
path: "screenshots/03-tooltip.png",
scale: "device",
});
await hideCursor(page);Switching UI states between screenshots
When the page has multiple configurations (radio buttons, tabs, toggles), select each state and capture it in sequence. For example, to screenshot each option in a radio group:
javascript
const states = [
{ label: "Option A", file: "02-option-a.png" },
{ label: "Option B", file: "03-option-b.png" },
];
for (const { label, file } of states) {
await page.getByLabel(label).click();
await page.waitForTimeout(300);
await showCursor(page, {
src: arrowUri,
x: 700, y: 500,
width: 32, height: 32,
});
await page.screenshot({
path: `screenshots/${file}`,
scale: "device",
});
await hideCursor(page);
}Verifying screenshot dimensions
On macOS, use sips to confirm output dimensions match expectations:
sh
sips -g pixelWidth -g pixelHeight screenshots/*.pngExpected output for a 1440×1024 viewport at 2x scale:
pixelWidth: 2880
pixelHeight: 2048Cleanup
Close the dedicated context after all screenshots are taken. If you created the context on an existing browser instance, don't close the browser itself:
javascript
await context.close();
// Don't close browser if other contexts are still in useTrade-offs
| Choice | Pro | Con |
|---|---|---|
deviceScaleFactor: 2 | Retina-crisp output | 4× file size vs 1x |
| Click-away-hover | Reliable tooltip trigger | More steps per shot |
| Zero-padded names | Correct sort order | Plan sequence upfront |