Why Are My Playwright Screenshots Blurry on High-DPI Screens?
Blurry Playwright screenshots often come from a mismatch between CSS pixels and device pixels. Set the screenshot scale and deviceScaleFactor to match your target.
Playwright screenshots can look blurry on high-DPI screens when the screenshot’s pixel scale does not match the display scenario you want to represent. Set the screenshot scale explicitly: use 'device' for one output pixel per device pixel, or 'css' for one output pixel per CSS pixel. Then check the browser context’s deviceScaleFactor, which is separate from viewport size.
Playwright’s page.screenshot() defaults to device scale, while screenshot assertions default to CSS scale. Those different defaults can make a capture and its visual test baseline have different dimensions. [Page screenshot API; screenshot assertions API]
1. Understand CSS pixels, device pixels, and screenshot scale
A CSS pixel is the unit used for layout. Device pixels are the physical-density pixels represented by the display or emulated device. The screenshot scale option controls how those units map to output image pixels:
| Scale | Output mapping | Use it when |
|---|---|---|
'css' |
One image pixel per CSS pixel | You want output dimensions aligned with the CSS layout viewport. |
'device' |
One image pixel per device pixel | You want a high-DPI capture with device-pixel detail. |
With a high-DPI context, a device-scale screenshot can have more output pixels than the CSS viewport dimensions. That is expected. Playwright documents 'device' as the page screenshot default; still specify it explicitly when the intended scale matters, so future changes or other capture paths do not create ambiguity. [Page screenshot API]
2. Set the screenshot scale explicitly
Here is a complete runnable Node.js example using Playwright’s Chromium browser. It creates a high-DPI context, opens a page, and saves a device-pixel screenshot. Install Playwright with npm install -D playwright and install Chromium with npx playwright install chromium, then save this as capture.mjs and run node capture.mjs.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 2,
});
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'capture-device.png', scale: 'device', fullPage: true });
await page.screenshot({ path: 'capture-css.png', scale: 'css', fullPage: true });
} finally {
await context.close();
await browser.close();
}
The two output files intentionally use different pixel scales. Compare their dimensions as well as their appearance. Use fullPage only if the capture should include the whole page; remove it for a viewport screenshot.
For an existing script, the smallest change is often simply:
await page.screenshot({ path: 'capture.png', scale: 'device' });
Choose 'css' instead when you need the image dimensions to correspond to CSS pixels. A larger pixel count does not by itself guarantee that an image will look sharper after some other tool resizes or displays it; inspect the saved image at its actual dimensions and account for any downstream scaling.
3. Configure high-DPI emulation separately
deviceScaleFactor belongs to the browser context configuration; it is not the screenshot scale option. Playwright documents a default factor of 1 and demonstrates 2 for high-DPI emulation. Set both the viewport and factor to match the scenario you want to reproduce:
const context = await browser.newContext({
viewport: { width: 2560, height: 1440 },
deviceScaleFactor: 2,
});
The viewport is specified in CSS pixels. The factor controls the emulated display density. Changing the viewport alone does not set the density, and changing the density does not choose a different CSS layout viewport. [Playwright emulation guide; BrowserType API]
For a real-device scenario, use the intended viewport and density together. For a standard-density desktop baseline, a factor of 1 may be appropriate. For a high-density scenario, Playwright’s example uses 2. Avoid changing these values casually between baseline creation and subsequent captures.
4. Make visual assertions use the intended scale
Visual regression assertions have their own screenshot options. Their documented default scale is 'css', unlike the page screenshot API’s documented 'device' default. If the baseline should correspond to device pixels, set the assertion scale explicitly. For CSS-pixel baselines, make that choice explicit too:
import { test, expect } from '@playwright/test';
test('page matches its visual baseline', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('example.png', {
scale: 'device',
fullPage: true,
});
});
Keep the assertion scale, browser context’s deviceScaleFactor, viewport, and baseline creation environment consistent. A mismatch can appear as a size difference, a visual diff, or a screenshot that looks soft when viewed at a different scale. [Page Assertions API]
5. Diagnose a blurry capture step by step
- Check image dimensions. Compare the saved PNG’s pixel dimensions with the CSS viewport. A device-scale capture on a high-DPI context can be larger than the viewport in CSS pixels.
- Inspect the screenshot call. Set
scaleto'device'or'css'based on the required output. Do not rely on an implicit default when capture and comparison must match. - Inspect context configuration. Check
deviceScaleFactorandviewportindependently. Confirm both reproduce the target display scenario. - Check whether this is an assertion. Screenshot assertions use a documented CSS-scale default. Configure their scale separately from direct calls to
page.screenshot(). - Compare the rendering environment. If dimensions and scale are correct but pixels still differ, check the operating system, browser version, settings, hardware, power source, and headless mode used for the baseline. Playwright identifies these as possible sources of rendering variation. [Visual comparisons]
- Inspect how the image is being viewed. A viewer, editor, or page may resize the file. Compare at native dimensions before concluding that the browser capture itself is blurry.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Direct screenshot looks sharper or has different dimensions than the test snapshot | The direct screenshot and assertion use different scale defaults. | Set scale explicitly in both capture paths. |
| High-DPI image has more pixels than the viewport numbers | scale: 'device' maps output to device pixels. |
Check whether those larger dimensions are intended. Use 'css' for CSS-pixel-sized output. |
| Changing viewport size did not fix softness | Viewport and deviceScaleFactor are separate settings. |
Set the context viewport and device scale factor to match the target independently. |
| Baseline diff persists with matching dimensions | Rendering environment may differ across runs. | Align OS, browser version, settings, hardware, power state, and headless mode as closely as practical. |
| File looks soft only in a report or editor | The image may be displayed at a scaled size. | Inspect the actual file dimensions and view it at native size; verify any downstream resizing. |
| Assertion option appears ineffective | The option may have been set on page.screenshot() but not on toHaveScreenshot(). |
Set the screenshot assertion’s own scale option. |
7. Performance, reliability, and cost
Device-scale output can contain more pixels than CSS-scale output for the same viewport, especially when the context emulates a high-density display. That increases the image dimensions; the precise effect on capture time, memory, and file size depends on the page and runtime. Use CSS scale when CSS-pixel dimensions are sufficient, and device scale when device-pixel output is required.
For repeatable visual tests, keep the viewport, deviceScaleFactor, screenshot scale, browser version, operating system, and headless mode consistent. Playwright notes that rendering can vary with host OS, version, settings, hardware, power source, and headless mode. Aligning these inputs reduces avoidable differences but does not imply every page will render identically across all environments. [Visual comparisons]
The remedy is a browser configuration decision: no display purchase is required to choose screenshot scale or emulate a high-DPI context. If capturing with a hosted screenshot API instead, check how that service handles output scale and billing before relying on it for a specific visual test workflow.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its API returns a screenshot or PDF from one GET request. See the ScreenshotNeo API documentation for request options. This example saves a WebP response:
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners are accepted like a visitor and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/).
FAQ
What scale should I choose for a high-DPI screenshot?
Choose 'device' when the output should represent device pixels. Choose 'css' when you want one output pixel per CSS pixel.
Does setting a larger viewport make the screenshot high-DPI?
No. The viewport and deviceScaleFactor are separate context settings. Set both to reproduce the intended scenario.
Why does my screenshot test look different from a saved page screenshot?
Direct page screenshots document a device-scale default, while screenshot assertions document a CSS-scale default. Set the scale explicitly in each API.
Will device scale fix every blurry screenshot?
No. First verify output dimensions and scale. If those are correct, inspect the display or report scaling and keep the rendering environment consistent across captures.


