ScreenshotNeo

BlogHow-to

Playwright Full-Page Screenshot Is Blank: Common Causes and Fixes

A blank Playwright capture can mean the page never rendered, or that full-page capture missed scroll-triggered content. Diagnose the difference and fix it.

By the ScreenshotNeo team4 October 20267 min read

If a Playwright full-page screenshot is blank, first take a viewport-only screenshot. If that is blank too, investigate navigation and application rendering; if only the full-page capture is blank or incomplete, check for content that loads on scroll, nested scroll containers, or page-specific readiness. fullPage: true captures the full scrollable page; it does not guarantee that asynchronous or scroll-triggered content has rendered.

This guide shows how to distinguish those cases, gather useful evidence, and capture the page after its content is ready. The exact cause cannot be determined without the page, browser and Playwright versions, platform, and capture options.

1. What Playwright full-page capture does

Playwright documents fullPage as capturing the full scrollable page instead of only the currently visible viewport. It controls the capture extent, not application readiness. A page may still be loading data, waiting for fonts, or withholding sections and images until they approach the viewport. Playwright Page screenshot API.

This distinction helps narrow the problem:

  • Viewport screenshot is blank too: start with navigation, application startup, authentication, failed scripts or resources, and page error handling.
  • Viewport is correct, but the full-page image is blank or incomplete: investigate lazy loading, scroll-triggered rendering, virtualization, document height, and nested scroll containers.
  • Capture stalls or times out: inspect the call log and environment, including browser engine and version, operating system, and font loading.

These are diagnostic branches, not guaranteed diagnoses. A particular page and reproduction are needed to identify the cause.

2. Diagnose the blank capture in order

  1. Capture the viewport. Temporarily omit fullPage: true. If the visible page is also empty, do not start by changing full-page options.
  2. Verify navigation and page state. Record the final URL, check whether the expected page loaded, and inspect the DOM for the content you expected to capture. Check authentication and any application error state.
  3. Wait for the actual content. Use a page-specific ready marker or locator for the relevant content. A navigation event or network condition alone may not mean the interface finished processing.
  4. Check content below the fold. Determine whether images, iframes, sections, or reveal effects appear only after scrolling. Scroll through the relevant regions, then verify the content loaded before taking the screenshot.
  5. Find the scrolling element. Check whether the document, body, or a nested panel owns the scrollable height. A full-page capture of the document does not necessarily represent a long panel with its own scrollbar.
  6. Record the environment if the issue persists. Note the Playwright version, browser engine and revision, OS, headed or headless mode, viewport, device scale factor, screenshot options, and screenshot call log.

Playwright issue reports discuss lazy images and frames, IntersectionObserver-gated content, scroll-triggered reveals, virtualized lists, and layouts that scroll inside a nested element. They are useful diagnostic leads, not proof of a universal browser defect: lazy-content discussion and nested scrolling report.

3. Wait for page-specific readiness

Use a locator or application-owned ready signal that represents the content needed in the image. In this runnable Node.js example, set PAGE_URL to the page and ensure that it exposes a test ID named page-ready. Replace that selector with a real marker from your app.

import { chromium } from 'playwright';

const url = process.env.PAGE_URL;
if (!url) throw new Error('Set PAGE_URL to the page you want to capture');

const browser = await chromium.launch();
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
  console.log('Final URL:', page.url());
  console.log('Navigation status:', response?.status() ?? 'no main response');

  // Replace this with an application-specific selector or ready marker.
  await page.getByTestId('page-ready').waitFor({ state: 'visible', timeout: 15000 });
  await page.screenshot({ path: 'viewport.png' });
  await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
  await browser.close();
}

Save as capture.mjs, install Playwright with npm install playwright, install its Chromium browser with npx playwright install chromium, then run PAGE_URL=https://example.com node capture.mjs. Use a URL and ready selector you are authorized to access. If the viewport image is blank, inspect navigation, the page DOM, console errors, failed requests, and authentication before focusing on full-page behavior.

4. Load content that appears on scroll

Lazy images, embedded frames, intersection-based rendering, and reveal effects may not run until their content nears the viewport. Scroll through the page or the actual scrollable panel, wait for the expected content, then return to the desired starting position and capture. A fixed sleep is only a rough aid: it cannot establish that a specific image or section is ready.

// Run after waiting for the page's initial ready marker.
await page.evaluate(async () => {
  const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  const step = Math.max(1, window.innerHeight);
  let previousHeight = 0;

  // Re-check height because loading content can extend the document.
  for (let pass = 0; pass < 10; pass++) {
    const height = document.documentElement.scrollHeight;
    for (let y = 0; y < height; y += step) {
      window.scrollTo(0, y);
      await pause(100);
    }
    await pause(250);
    const newHeight = document.documentElement.scrollHeight;
    if (newHeight === height && newHeight === previousHeight) break;
    previousHeight = newHeight;
  }
  window.scrollTo(0, 0);
});

// Prefer checking a real expected element or image before this capture.
await page.screenshot({ path: 'full-page.png', fullPage: true });

This is an illustrative scroll pass, not a universal lazy-loading solution. Adapt its stopping condition and readiness checks to the application. If a nested panel owns scrolling, scroll that element instead of the window:

const panel = page.locator('[data-testid="results-panel"]');
await panel.evaluate(async element => {
  const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
  const step = Math.max(1, element.clientHeight);
  for (let y = 0; y < element.scrollHeight; y += step) {
    element.scrollTop = y;
    await pause(100);
  }
  element.scrollTop = 0;
});

Replace [data-testid="results-panel"] with the real scroll container. A virtualized list may only keep visible rows in the DOM; scrolling can cause earlier rows to be removed while later ones appear. In that case, one full-page screenshot may not represent every item at once. Use an application-supported non-virtualized export or capture the required ranges separately.

5. Common causes and fixes

Symptom What to inspect Targeted fix
Viewport and full-page captures are both blank Navigation result, final URL, app startup, authentication, page DOM, console errors, and failed requests Fix the navigation or application-rendering problem; wait for an app-specific ready state before capturing.
Viewport looks right; lower sections or images are missing Lazy loading, IntersectionObserver, scroll-triggered reveals, frames, or virtualization Scroll the relevant regions or container, wait for the expected content, verify it, and then capture.
The page is a panel with its own scrollbar Which element owns scrollHeight and changes scrollTop Scroll and capture the relevant element or use an app-specific approach; document full-page height may not cover the panel.
Screenshot call times out while waiting for fonts Full call log, font requests, Playwright/browser version, OS, and whether the issue reproduces in another environment Reproduce with the same page and recorded environment. One open report describes Linux WebKit on Playwright 1.63.0 timing out at font readiness and a 1.60.0 control succeeding; it does not establish a general cause or resolution. Reported issue.
Visual output differs between local and CI OS, browser build, installed fonts, viewport, device scale factor, and headed/headless configuration Standardize the rendering environment before changing app code.
Capture is unexpectedly slow or grows indefinitely Infinite scroll, continuously appended content, animations, and changing document height Define a finite capture boundary, stop condition, or app-specific export. Do not wait for an endless page to become complete.

6. Keep visual captures consistent

For screenshot comparisons, keep the browser engine and version, host OS, fonts, viewport, device scale factor, and headed or headless mode consistent. Playwright notes that rendering can vary with operating system, browser version, settings, hardware, power source, and headless mode. Playwright visual comparisons.

When diagnosing a report, include the Playwright version, browser engine and revision, operating system, viewport and scale factor, page readiness marker, scroll-container structure, whether viewport-only capture is blank, and the screenshot call log. This turns “blank on CI” into a reproducible comparison.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. If you need a clean capture without installing and maintaining a browser in your own script, its one-call API can return an image or PDF. Read the ScreenshotNeo 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,
)
r.raise_for_status()
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);

The Node.js example uses Bun’s file writer to save the response body. With Node.js, replace the final line with import { writeFile } from 'node:fs/promises'; await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));.

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Other options include full-page capture, element selection, custom CSS and JavaScript, waiting for a selector or network idle, viewport and device presets, PDF output, and async jobs. Review the docs for supported parameters and behavior.

Sign up free for 1,000 screenshots a month with no card.

8. FAQ

Does fullPage: true scroll through the page for me?

It requests a capture of the full scrollable page. Do not assume it triggers every site’s viewport-based loading behavior; test the page and scroll through relevant content when needed.

Should I always use networkidle?

No single navigation or network signal proves that the specific content you need is ready. Wait for an application-specific marker or locator, then verify the content.

Can one full-page image include every row of an infinite list?

Not necessarily. A virtualized list may render only a moving window of rows, and an infinite list has no natural end. Define a finite scope or use an application-supported export.

Why does the screenshot look different on another machine?

Browser rendering can vary with the environment. Align the browser, OS, fonts, viewport, scale factor, and headless configuration before treating pixel differences as an application bug.