ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Does Not Show Content Lower Down the Website

Fix missing lower-page content in Puppeteer screenshots. Learn when to use fullPage, how to handle lazy loading and nested scroll areas, and how to diagnose clipped captures.

By the ScreenshotNeo team4 October 20269 min read

If a Puppeteer screenshot shows only the top of a website, first check whether you asked for a viewport screenshot or the whole document. For the whole document, use fullPage: true; its default is false. If lower content is still blank or absent, it may be lazy-loaded, inside its own scrolling panel, not ready when the screenshot runs, or outside a configured clip.

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

fullPage requests a screenshot of the full page. It does not guarantee that application content or assets that have not loaded or rendered will appear. The fix depends on which kind of content is missing.

1. Confirm what the screenshot should include

A normal screenshot captures the current viewport. If you want only what is currently visible, the missing lower page is expected; increase the viewport height if that is the intended result. If you want the document from top to bottom, explicitly set fullPage: true.

Goal What to do
Capture what is currently visible Use a normal screenshot and set the viewport to the desired dimensions.
Capture the full document Use fullPage: true.
Capture one rectangular region Use a clip rectangle and check its coordinates and dimensions.
Capture content inside a scrolling panel Scroll that panel or capture it separately; a full-document screenshot does not reveal every panel’s hidden scroll contents.

2. Use a page-specific readiness condition

Start navigation, wait for a condition that represents the page state you need, and then take the full-page screenshot. Replace the example selector with a real element that appears when the relevant content is ready.

const url = 'https://example.com';
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#main-content');
await page.screenshot({ path: 'page.png', fullPage: true });

waitUntil controls when navigation is considered complete. domcontentloaded means the document has been parsed; it does not promise that every image, script-driven section, or application update has completed. Choose a selector or another condition that reflects the content you need.

When network idle helps—and when it does not

page.waitForNetworkIdle() can help when the page makes a burst of requests and then becomes quiet. Its documented default idle interval is 500 ms. Network inactivity does not prove that a specific section has been inserted, that an image has loaded successfully, or that the page has finished painting the state you expect. Prefer an explicit page-specific readiness condition where possible.

await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#main-content');
await page.waitForNetworkIdle();
await page.screenshot({ path: 'page.png', fullPage: true });

Use network idle as an additional signal, not as a substitute for identifying the missing content’s readiness. Pages with ongoing polling or analytics may also fail to become idle under the chosen settings.

3. Load content that appears only after scrolling

Some pages defer images or sections until they approach the viewport. A full-page screenshot does not establish that all such content was triggered and loaded. Check whether the missing area appears when you scroll there manually. If it does, scroll the page in steps before capturing and wait for a meaningful condition after each step.

const url = 'https://example.com';
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#main-content');

const step = 700;
await page.evaluate(async (stepSize) => {
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const maxScroll = document.documentElement.scrollHeight;
  for (let y = 0; y < maxScroll; y += stepSize) {
    window.scrollTo(0, y);
    await pause(150);
  }
  window.scrollTo(0, 0);
}, step);

// Replace this with a selector or condition that confirms the needed content loaded.
await page.waitForSelector('#lower-section');
await page.screenshot({ path: 'page.png', fullPage: true });

The short pause above is an example of pacing scroll steps, not a guarantee that a particular site is ready. A selector, image load check, or application-specific state is more dependable. If the document grows while you scroll, a single initial scrollHeight can become stale; measure it again as needed, or scroll until the page stops growing and the target content is present.

4. Check for nested scrolling containers

A page may have a fixed-height panel whose contents scroll independently of the document. In that layout, the document’s scrollHeight may be small even though the panel contains more content. Inspect the element that owns the scrolling and scroll that element into view or operate its scroll position before capturing.

const panel = page.locator('.results-panel'); // replace with the actual scroll container
await panel.scrollIntoView();
await panel.evaluate(async (el) => {
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  for (let y = 0; y < el.scrollHeight; y += 600) {
    el.scrollTop = y;
    await pause(150);
  }
});
await page.screenshot({ path: 'panel-state.png' });

Scrolling a panel changes what is visible inside that panel; it does not turn its entire scrollable contents into a taller document. If you need every item, consider capturing the panel in sections or using page-specific code to expand or render all its contents. The correct selector and strategy depend on the site’s layout.

5. Inspect clipping and viewport settings

If you pass clip, verify the rectangle’s x, y, width, and height. A clip can intentionally restrict the output. captureBeyondViewport is a separate option; its documented default depends on whether a clip is provided. Check the current Puppeteer documentation for its behavior and set it deliberately when you use a clipped capture. It is not a replacement for fullPage.

await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 1200, height: 2400 },
  captureBeyondViewport: true
});

For an ordinary full-document capture, begin with fullPage: true and remove a clip unless you need one. Also verify that the viewport is large enough for a viewport-only capture and that the selected device scale factor is not producing an unexpectedly large output.

6. Diagnose the page before capturing

Compare the document dimensions with the screenshot and check whether the missing text or image exists before capture. These checks help separate a capture-option issue from content that has not loaded.

const diagnostics = await page.evaluate(() => ({
  viewportHeight: window.innerHeight,
  documentHeight: document.documentElement.scrollHeight,
  bodyHeight: document.body?.scrollHeight ?? null,
  targetExists: Boolean(document.querySelector('#lower-section')),
  targetText: document.querySelector('#lower-section')?.textContent?.trim().slice(0, 200) ?? null
}));
console.log(diagnostics);

If the target is absent from the DOM, investigate navigation, application state, lazy loading, or the correct frame. If it exists but is not visible, inspect its bounding box, hidden styles, overlays, and scrolling ancestor. For images, check whether the relevant image elements have completed loading and have a nonzero natural width.

Runnable example with Puppeteer

This Node.js example launches Chromium, opens a page, waits for a page-specific selector, and saves a full-document screenshot. Install Puppeteer in your project with npm install puppeteer, then save the code as capture.cjs and run node capture.cjs https://example.com.

const puppeteer = require('puppeteer');

async function main() {
  const url = process.argv[2];
  if (!url) throw new Error('Pass a URL: node capture.cjs https://example.com');

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
    await page.waitForSelector('body', { timeout: 15000 });
    // Replace #main-content with a selector that indicates the needed content is ready.
    // await page.waitForSelector('#main-content', { timeout: 15000 });
    await page.screenshot({ path: 'page.png', fullPage: true });
    console.log('Saved page.png');
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The example uses the broadly available puppeteer package and CommonJS. If your project uses ES modules, import Puppeteer with import puppeteer from 'puppeteer';. Navigation options, selectors, and scroll behavior should reflect the target site rather than be copied blindly.

Or skip the browser setup

For a one-call screenshot API, ScreenshotNeo returns an image or PDF from a URL. Its options include full-page capture with lazy images loaded, selector-based element capture, custom waits, viewport and device presets, and image formats. See the ScreenshotNeo API docs.

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use screenshot, page-info, and PDF-capture tools. 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 for 1,000 screenshots a month with no card.

Other useful ScreenshotNeo request examples

The same endpoint can be called from cURL or Python. These examples save the returned bytes to a file. For available parameters and response details, use the API documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Troubleshooting checklist

Symptom Likely cause What to check or change
Only the visible top appears fullPage is omitted or false Set fullPage: true for a document screenshot.
Bottom section is blank It may load after scrolling or after an application update Scroll to trigger it, then wait for a selector or other meaningful readiness condition before capture.
Panel content is missing The panel scrolls independently from the document Find and scroll the panel; capture portions separately if the complete panel cannot fit in one view.
Screenshot ends at an unexpected boundary A clip restricts the captured area Review clip coordinates and dimensions; check captureBeyondViewport when using a clip.
Network-idle wait times out The page may keep making requests Use a page-specific selector or state instead, or configure the wait based on the page’s actual behavior.
Selector wait times out The selector may be wrong, in another frame, or never inserted Inspect the DOM, confirm the selector and frame, and check whether the page requires scrolling or interaction.
Images are missing but surrounding text appears Images may still be loading, be lazy-loaded, or have failed Trigger lazy loading by scrolling and inspect each relevant image’s load state and natural dimensions.
Capture is unexpectedly huge or slow A very long page, high device scale factor, or large assets can increase output work Capture only the needed area, lower the viewport scale when suitable, or split the capture into sections.

Performance, reliability, and cost

  • Wait for the right condition. A narrow readiness condition avoids arbitrary long delays while reducing the chance of capturing too early. Network idle is only a signal about requests.
  • Account for long pages. Full-page images can consume substantial memory and take longer to encode as document dimensions grow. Capture sections or a target element when the whole document is unnecessary.
  • Be deliberate about retries. If navigation or an asset fails, diagnose the failing condition and retry only when appropriate. Repeatedly taking the same early screenshot will not make late content ready.
  • Control your browser environment. Puppeteer runs Chromium, so browser version, viewport, device scale factor, fonts, and network conditions can affect output. Record these values when comparing captures.
  • Cost depends on where it runs. Self-hosted Puppeteer has no per-shot API charge, but uses compute, memory, and maintenance time. A hosted screenshot API trades browser setup for its plan pricing; ScreenshotNeo’s Free plan has 1,000 shots per month with no card, and paid plans start at $5 for 3,000.

Frequently asked questions

Does fullPage: true load every item on a page?

No. It requests a full-document screenshot; it does not prove that lazy content, application state, or assets have loaded. Trigger and verify the content the page requires.

Should I use a fixed timeout?

Use a fixed delay only when a known delay is part of the page behavior. Prefer waiting for the specific selector, image, or state you need.

Will a full-page screenshot include every item in a scrolling panel?

Not necessarily. A panel with its own scrollbar is separate from document scrolling. Scroll or capture that panel according to the page layout.

What details help diagnose a persistent issue?

Share the screenshot call, Puppeteer and Chromium versions, target URL or a minimal reproducer, viewport and device scale factor, whether a clip is used, and whether the missing area appears after scrolling. Also note whether the page uses an iframe or nested scroll container.

References