ScreenshotNeo

BlogHow-to

Why Full-Page Screenshots Fail and How to Fix Them

Fix missing content in full-page screenshots by finding the real scroll container, rendering lazy content, and choosing the right capture target.

By the ScreenshotNeo team1 October 20265 min read

Short answer: A full-page screenshot expands the document’s scrollable height. It does not automatically expand a nested scrolling panel, trigger every scroll event, or materialize all rows in a virtualized list. Find which element owns the scrollbar, make scroll-dependent content render, then choose a document or element capture that matches your target.

The same diagnosis applies whether you use Playwright, Puppeteer, a browser extension, or a screenshot API.

1. Identify what is actually scrollable

Open the page in a browser and move the pointer over the missing area. If the browser scrollbar moves, the document owns the scroll. If only a panel scrollbar moves while the document stays fixed, the panel is a nested scroll container. Document full-page mode does not expand every nested container; this is documented in the Playwright issue discussion.

Need Target Typical API
Long article or landing page Document fullPage: true
Scrollable dashboard panel Panel element Locator or element screenshot
One card or component Component element Locator or element screenshot
Every row in a virtualized list Several viewport captures or data export Scroll-and-capture workflow

2. Capture a whole document with Playwright

Playwright’s page.screenshot({ fullPage: true }) captures the full page area (Page screenshot API).

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com/article', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();

For a component, target the locator:

const panel = page.locator('[data-testid="results-panel"]');
await panel.screenshot({ path: 'panel.png' });

3. Capture a whole document with Puppeteer

Puppeteer defines fullPage as taking a screenshot of the full page. Its options also include captureBeyondViewport and clipping (ScreenshotOptions reference).

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com/article', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();

For a nested scroller:

const panel = await page.$('[data-testid="results-panel"]');
if (!panel) throw new Error('results panel not found');
await panel.screenshot({ path: 'panel.png' });

captureBeyondViewport controls the capture area; it does not make lazy content appear or replace a virtualized data model.

4. Render content that depends on scrolling

Lazy images, iframes, IntersectionObserver callbacks, reveal animations, and virtualized lists may wait until an item approaches the real viewport. A full-page rasterization can extend beyond the viewport without replaying the scroll events your application expects. See the Playwright scroll-dependent content discussion.

async function renderEverything(page) {
  await page.evaluate(async () => {
    await new Promise(resolve => {
      const step = Math.max(1, window.innerHeight - 80);
      let y = 0;
      const timer = setInterval(() => {
        window.scrollBy(0, step);
        y += step;
        if (y >= document.documentElement.scrollHeight) {
          clearInterval(timer);
          window.scrollTo(0, 0);
          resolve();
        }
      }, 100);
    });
  });
  await page.waitForTimeout(500);
}

await renderEverything(page);
await page.screenshot({ path: 'rendered-page.png', fullPage: true });

Prefer page-specific readiness signals, such as a known image, a loading spinner disappearing, or a rendered-card count. A fixed delay is only a fallback.

5. Capture a nested scrolling panel

If the panel has overflow: auto or overflow: scroll, document full-page mode usually leaves it at its viewport height.

const panel = page.locator('.feed');
await panel.scrollIntoViewIfNeeded();
await panel.evaluate(el => {
  el.scrollTop = el.scrollHeight;
});
await page.waitForTimeout(300);
await panel.screenshot({ path: 'feed-panel.png' });

This captures the panel’s rendered box. If you need one tall image, temporarily expand the element in the test page or stitch viewport captures, and document that layout change.

6. Handle virtualized lists correctly

Virtualized lists keep only a window of rows in the DOM and replace earlier rows as you scroll. One screenshot therefore cannot guarantee every logical record.

const list = page.locator('[role="list"]');
for (let i = 0; i < 5; i++) {
  await list.evaluate(el => el.scrollBy(0, el.clientHeight));
  await page.waitForTimeout(250);
  await list.screenshot({ path: `list-${i}.png` });
}

7. Check fixed and sticky elements

Headers, cookie notices, chat buttons, and sticky sidebars can repeat or cover content. Use the same viewport and device scale as the intended display. Hide test-only overlays or capture the content element directly.

8. Diagnostic checklist

  1. Determine which scrollbar moves the missing content.
  2. Inspect for overflow: auto, fixed heights, and virtualized list roots.
  3. Check whether missing nodes exist before scrolling.
  4. Wait for images, iframes, fonts, animations, and loading indicators.
  5. Use page full-page mode for documents and element capture for components.
  6. Review for missing ranges, overlays, and repeated sticky controls.
  7. For virtualized content, capture ranges or use a data export.

9. Common errors and fixes

Symptom Cause Fix
Image ends at viewport bottom fullPage omitted or wrong target Use page full-page mode for a document; element capture for a panel.
Panel content missing Panel owns scrolling Capture the panel or change the test layout.
Lower images blank Lazy loading never received scroll events Scroll in steps and wait for readiness.
Rows disappear List is virtualized Capture ranges or export records.
Repeated bars Sticky or fixed UI Hide it in a test stylesheet or target content.
Timeout Network never reaches idle Wait for a specific selector and inspect failed requests.
Cut off after navigation Layout not settled Wait for final URL, selectors, fonts, and images.

10. Reliability and performance

  • Set deterministic viewport, device scale, timezone, and locale values.
  • Prefer explicit readiness signals over arbitrary sleeps.
  • Bound every wait and report useful diagnostics.
  • Block irrelevant analytics and ads, but keep resources that affect layout.
  • Use JPEG or WebP when lossless pixels are unnecessary.
  • Repeat dynamic captures when time, ads, or personalization can change output.

11. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. Its full-page option loads lazy images, and CSS selectors can target a panel. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers. See 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)
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}`);

Options include full-page capture, CSS selectors, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, selector or network-idle waits, blocked resources, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs, PDFs, HTML/CSS input, and usage reporting. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Free includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.

12. FAQ

Does fullPage scroll the page?

It captures the document’s full area, but scroll-dependent application behavior may still need explicit scrolling.

Can I make a nested panel part of the full page?

Capture the panel or change the layout so its content belongs to document flow.

Why is a virtualized table incomplete?

Rows outside the render window may not exist in the DOM. Capture ranges or use the data interface.

Which capture is best for a component?

Use a locator or element screenshot.

How do I verify ScreenshotNeo’s result?

Inspect the X-Page-Verdict and X-Billed response headers.