How to Fix Blank Images in Playwright Website Screenshots
Fix blank images in Playwright screenshots by checking the image’s source and load state, waiting for the right condition, and tracing failed requests.
A blank image in a Playwright screenshot usually means the image was not ready when capture happened, its request failed, or the screenshot targeted a different area than expected. Check the specific <img> element’s source and decoded image state, then wait for that condition before capturing. A page load state—especially networkidle—does not prove that a particular image is ready.
This guide uses Playwright’s JavaScript/TypeScript API. The same diagnostics apply in other Playwright language bindings, though their syntax differs. The Frame API documents image-loading waits, and the Page API documents navigation, screenshot, and wait behavior.
1. Check whether the image itself loaded
For an image element, inspect currentSrc, complete, and naturalWidth. A completed image with naturalWidth === 0 has not produced usable decoded image pixels. The selected source can differ from the literal src, for example when responsive image markup is involved.
const image = page.locator('img.hero');
console.log(await image.evaluate(img => ({
src: img.getAttribute('src'),
currentSrc: img.currentSrc,
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
loading: img.loading
})));
Adapt the selector to the page. If the visible image is a CSS background, canvas, or an application-rendered component rather than an <img>, this check will not cover it; inspect the relevant element, request, and application state instead.
2. Wait for the target image, then capture
Wait for the image to be visible and for its decoded width to become nonzero. This is a target-specific condition rather than a guess about how long the entire page might take.
import { chromium, expect } from '@playwright/test';
const browser = await chromium.launch();
const page = await browser.newPage();
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded'
});
console.log('Main document status:', response?.status());
const image = page.locator('img.hero');
await image.waitFor({ state: 'visible' });
await expect.poll(async () => image.evaluate(img => {
const el = img as HTMLImageElement;
return el.complete && el.naturalWidth > 0;
}), { message: 'Hero image should finish loading and decode' }).toBe(true);
await page.screenshot({ path: 'page.png' });
await browser.close();
Install the packages with npm install -D @playwright/test and install the browser with npx playwright install chromium. Run the file with your project’s configured Playwright runner, or adapt the snippet into a test. The example’s URL and selector are placeholders. If the condition times out, treat that as a diagnostic result: inspect the actual image request and page errors rather than simply extending the timeout.
When the image is lazy-loaded or appears after interaction
Below-the-fold images may not start loading until scrolled into view. Scroll the target into view, perform any required interaction, and then wait on the image condition. A single navigation event cannot account for work triggered later by scrolling or app logic.
await image.scrollIntoViewIfNeeded();
await image.waitFor({ state: 'visible' });
await expect.poll(() => image.evaluate(img => {
const el = img as HTMLImageElement;
return el.complete && el.naturalWidth > 0;
})).toBe(true);
If the page swaps sources after a user action, wait for the application’s resulting state or expected currentSrc as well. A selector becoming visible alone does not guarantee its pixels have loaded.
3. Choose a useful navigation readiness signal
| Signal | What it tells you | When it helps |
|---|---|---|
domcontentloaded |
The document was parsed; later resources or application work may still be pending. | When you will wait explicitly for the content you need. |
load |
The page load event fired after its load-dependent resources. | When the page’s relevant content is covered by this lifecycle event, while still verifying the target image. |
networkidle |
Playwright defines this as no network connections for at least 500 ms. | It can be useful in limited situations, but it is not a universal image-ready signal. |
| Image or app assertion | The particular image or application state reached the condition you check. | Preferred when screenshot correctness depends on specific content. |
Playwright explicitly discourages using networkidle as a test readiness strategy and recommends web assertions to assess readiness. It also marks fixed waitForTimeout waits as discouraged for production tests. A quiet network can coexist with a failed image, and a page can continue background requests after the image you need is ready. See the current Page API and Frame API for version-specific details.
4. Inspect failed requests and page errors
If the image never satisfies the readiness check, determine whether its request was made and what happened. Record responses, request failures, console errors, and page errors. These event handlers should be registered before navigation so early failures are not missed.
page.on('requestfailed', request => {
console.error('Request failed:', request.url(), request.failure()?.errorText);
});
page.on('response', response => {
if (response.request().resourceType() === 'image' && !response.ok()) {
console.error('Image response:', response.status(), response.url());
}
});
page.on('console', message => {
if (message.type() === 'error') console.error('Console:', message.text());
});
page.on('pageerror', error => console.error('Page error:', error));
const response = await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Navigation response:', response?.status());
Use the evidence to check the selected image URL, response status, access requirements, and page-specific rendering behavior. A wrong URL, restricted resource, failed response, or delayed application behavior are possible causes, but none can be assumed without the failing page’s request and error details. Playwright navigation can also fail when the main document resource fails; retain the navigation response or exception for diagnosis.
5. Capture the intended scope
For a viewport or full-page image, use page.screenshot. For a single component, use a locator screenshot so the target is explicit. Playwright recommends locator screenshots over the discouraged ElementHandle.screenshot method; see the ElementHandle API.
// Viewport
await page.screenshot({ path: 'viewport.png' });
// Full scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });
// One element
await page.locator('.product-card').screenshot({ path: 'card.png' });
If an image appears outside the viewport but the output is a viewport screenshot, the screenshot may simply not include it. If the target itself is correct but the output is visually unstable, Playwright Test’s toHaveScreenshot waits for two consecutive screenshots to match before comparing them. That manages visual stability; it does not prove that every image request succeeded. Pair it with an explicit image readiness check. See the PageAssertions API.
6. Troubleshooting common blank-image cases
| Symptom | Likely diagnostic branch | What to do |
|---|---|---|
naturalWidth stays at zero |
The selected image has not decoded successfully, or the source is not usable. | Check currentSrc, whether the request occurred, its response, and console errors. |
| No image request appears | The image may be lazy-loaded, not yet inserted, or only requested after interaction. | Confirm the selector, scroll into view, trigger the expected interaction, then observe again. |
| Request fails or returns an error status | The resource was not successfully retrieved. | Use the recorded URL, failure text, and status to investigate source correctness and access behavior. |
networkidle wait hangs or times out |
Ongoing background traffic may prevent idleness. | Wait for the target image or app condition instead. |
| Image is present in the DOM but absent from output | Capture scope, visibility, or timing may be wrong. | Verify viewport versus full-page capture, scroll/visibility, and use a locator screenshot for the component. |
| Screenshot comparison is inconsistent | Rendering may still be visually unstable, or resources differ across runs. | Use screenshot assertions for visual stabilization and independently assert the target image loaded. |
| Longer fixed delay appears to help intermittently | The underlying readiness condition is variable. | Replace the delay with a condition tied to the image or application; retain a timeout as a failure bound. |
7. Performance, reliability, and cost
Waiting for one image or a small set of required images is generally more efficient and predictable than waiting for every connection to become idle. For a page with many important images, check the relevant set with a bounded assertion and report which elements failed. Keep a finite timeout so a broken resource produces a useful failure instead of an indefinitely stalled capture.
For repeatable captures, make readiness explicit, preserve request diagnostics on failure, and use stable test data where the page allows it. Network and site behavior can vary, so no fixed delay guarantees correctness. A visual snapshot assertion helps detect rendered changes but cannot substitute for request success or image decoding checks. The direct Playwright approach has no per-screenshot API charge; its operational cost is the browser runtime and the engineering time needed to operate and diagnose captures.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. One GET request returns a PNG, JPEG, WebP, or PDF; see the API documentation.
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}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and the response identifies the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does img.complete mean the image is usable?
No. Check naturalWidth > 0 as well; a completed failed image can have no usable intrinsic pixels.
Should I always wait for load before a screenshot?
No. Choose a lifecycle or application signal appropriate to the page, then assert that the image relevant to the capture is ready.
Will toHaveScreenshot catch a failed image request?
It checks screenshot stability and compares pixels. Add an explicit image-state assertion when successful image loading matters.


