# Puppeteer screenshot blank, white, black or blurry: causes reproduced and fixed

> Why a Puppeteer screenshot comes out white, black or blurry: each cause reproduced on a test page, with the fix and the measured result.

Published 2026-11-08 · https://shareglint.com/en/guides/puppeteer-screenshot-blank-or-blurry

A Puppeteer screenshot that comes out white, black or soft is one of a handful of causes, and each has a one-line fix once you know which one you have. On this page every cause is reproduced on a small test page, with the measured result before and after the fix.

**How we tested.** On 2026-11-08, Puppeteer 25.7 with its bundled headless Chrome, against a local Node server with four pages: a single-page app that fetches its content (the API answers after 800 ms), a page that fades in over 1.2 s, a page with a transparent background, and a paragraph of 16 px text. "Ink" below is the share of pixels that are not near-white (under 230 of 255 in grey): 0% means an empty white picture.

## White: the content is not there yet

```javascript
await page.goto('http://localhost:8098/spa', { waitUntil: 'load' });
await page.screenshot({ path: 'spa.png' });   // ink 0.00% — completely white
```

The HTML of a single-page app is an empty `<div id="app">`. The `load` event fires when that HTML, its script and its images are in — before the script has fetched the data and drawn the page. The screenshot shows exactly what was there: nothing.

Two fixes, both gave the full page (ink 2.23%):

```javascript
// Wait for an element of the real content (best: precise, no idle guessing).
await page.goto('http://localhost:8098/spa');
await page.waitForSelector('#title');

// Or wait for the network to go quiet (works when the content needs a request).
await page.goto('http://localhost:8098/spa', { waitUntil: 'networkidle0' });
```

`networkidle0` waits until no request has been open for 500 ms. It worked here, but on pages with analytics beacons, polling or a chat widget it waits for its full timeout or never settles; [waiting for page load](https://shareglint.com/en/guides/puppeteer-wait-for-page-load) measures the options side by side. A selector of the content is the dependable signal.

## White again: the page is fading in

```javascript
await page.goto('http://localhost:8098/fade');
await page.screenshot({ path: 'fade.png' });   // ink 0.00% — the text is at opacity 0
```

Landing pages often animate their content in: `opacity: 0` to `1` and a small slide over a second or so. Right after `goto` the animation has only started, so the capture is white, or in a longer animation half-transparent. Both fixes gave the finished page (ink 3.32%):

```javascript
// Ask for no motion — most sites honour it (this one has a reduced-motion rule).
await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]);
await page.goto('http://localhost:8098/fade');

// Or wait until every running animation has finished.
await page.goto('http://localhost:8098/fade');
await page.evaluate(() => Promise.all(document.getAnimations().map((a) => a.finished)));
```

[`document.getAnimations()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations) returns the CSS animations and transitions running in the document; waiting on their `finished` promises ends when the last one does. Do not use it on a page with an infinite animation (a spinner): that promise never resolves. [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) only helps on sites that have a rule for it.

## Black: a transparent picture shown without a background

```javascript
await page.goto('http://localhost:8098/clear');
await page.screenshot({ path: 'clear.png', omitBackground: true });
```

[`omitBackground`](https://pptr.dev/api/puppeteer.screenshotoptions) "hides default white background and allows capturing screenshots with transparency". On a page without its own background colour, the PNG is transparent everywhere except the text. Opened in a viewer with a dark background, or converted to RGB without a background — Pillow's `convert("RGB")` gave a picture of mean brightness 1 out of 255 — it looks black. The same option with `type: 'jpeg'` came out white, because JPEG has no transparency and Chrome fills it.

Fix: leave `omitBackground` out unless you need transparency, or flatten the PNG onto white when you convert it. If the page itself sets a dark background, the screenshot is right and the page is dark.

Other black or empty pictures we did not reproduce here but are common: WebGL and `<canvas>` content in a headless Chrome without GPU support, and video elements, which may show their poster or nothing. For those, test with a headed browser to see whether the page draws them at all in your setup.

## Blurry: too few pixels for the screen

With the default `deviceScaleFactor: 1`, an 800×400 viewport gives an 800×400 picture. That is correct, but a retina laptop or a phone shows it at two or three physical pixels per picture pixel, so text looks soft. With `deviceScaleFactor: 2` the same page gave a 1600×800 picture with the text drawn at full resolution — 39 KB as PNG instead of 15 KB.

```javascript
await page.setViewport({ width: 800, height: 400, deviceScaleFactor: 2 });
```

Two more causes of blur:

- **JPEG at low quality.** At `quality: 30` the 16 px paragraph got grey blocks and halos around every letter; the file shrank from 15 KB (PNG) to 8.7 KB. For screenshots with text, use PNG or WebP, or JPEG at 80 and above.
- **Scaling after capture.** A picture enlarged in an `<img>`, a slide or a document is resampled and blurred. Capture at the size you show it at, or larger, and only ever shrink. [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in the target browser tells you how many pixels per CSS pixel it needs.

## A checklist

| Symptom | Cause | Fix |
|---|---|---|
| All white | Content drawn by script after `load` | `waitForSelector` of the content, or `networkidle0` |
| White or faint | Fade-in animation running | `prefers-reduced-motion: reduce`, or wait for `getAnimations()` |
| Black | Transparent PNG viewed or converted without a background | Drop `omitBackground`, or flatten onto white |
| Soft text | `deviceScaleFactor: 1` shown on a retina screen | `deviceScaleFactor: 2` |
| Blocky text | Low JPEG quality | PNG/WebP, or JPEG ≥ 80 |
| Missing images further down | Lazy loading, below the fold | Scroll through first; see [full-page screenshots](https://shareglint.com/en/guides/puppeteer-full-page-screenshot) |
| Wrong page, no error | Screenshot during a redirect | Wait for the final page; see [execution context destroyed](https://shareglint.com/en/guides/puppeteer-execution-context-destroyed) |

## The same fixes in our API

If the job is "a good picture of a public URL", our screenshot API sets these for you or exposes them as options: it waits for the DOM, the load event (up to a few seconds) and a quiet network before capturing; `wait_for` waits for a selector like `waitForSelector`; `delay` adds seconds; `reduced_motion: true` sets `prefers-reduced-motion: reduce`; `scale` 2 or 3 gives retina pictures; `quality` defaults to 85 for JPEG and WebP; `lazy_load` scrolls through the page first. It does not wait for CSS animations by itself, so on a fade-in page add `reduced_motion` or a `delay`.

```bash
curl -X POST "https://shareglint.com/v1/screenshots?wait=true" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/", "wait_for": "#app h1", "reduced_motion": true, "scale": 2}'
```

A viewport capture is 1 render of the quota, a full page 2 ([pricing](https://shareglint.com/en/pricing)). For your own Puppeteer code, the basics are in [Puppeteer screenshots](https://shareglint.com/en/guides/puppeteer-screenshot).

## FAQ

### Why is my Puppeteer screenshot blank or white?

The picture was taken before the page drew anything. The two causes we reproduced: a single-page app whose content arrives after the load event (100% white at load), and a page that fades in with a CSS animation (still at opacity 0 when captured). Wait for an element of the content (waitForSelector), or for the animations to finish, before page.screenshot().

### Why is my Puppeteer screenshot black?

Usually it is a transparent PNG shown or converted without a background. With omitBackground: true, Puppeteer saves transparent pixels; a viewer with a dark background, or a conversion such as Pillow's convert("RGB"), shows them black. Drop omitBackground or flatten the image onto white. As JPEG, the same page came out white.

### How do I make Puppeteer screenshots sharper?

Set deviceScaleFactor: 2 (or 3) in page.setViewport. At 1, a 800×400 viewport is an 800×400 picture, and on a retina screen every pixel is stretched over four. At 2 it is 1600×800, with the text drawn at the screen's real resolution. Use PNG for text; JPEG at low quality adds grey artefacts around letters.
