ScreenshotNeo

BlogHow-to

Why Does a Full-Page Screenshot Omit Content Inside an Overflow Div?

A full-page screenshot captures the document’s scrollable area, not every nested scroll container. Learn how to diagnose and capture the missing content.

By the ScreenshotNeo team4 October 20269 min read

A full-page screenshot usually captures the document’s scrollable extent. It does not automatically expand every nested element with its own scrollbar. If a div has a constrained height and overflow: auto or overflow: scroll, the div scrolls independently; its offscreen content does not add to the document’s height, so the screenshot may show only the part visible in the page layout.

To capture that content, first identify the element that actually scrolls. For static content, temporarily expand the panel before capturing. For dynamic or virtualized content, scroll the panel in steps, wait for new content, and capture and stitch the views. If the panel is intentionally part of the interface, preserve its normal appearance when fidelity matters.

1. Understand what “full page” means

CSS overflow determines what happens when content exceeds an element’s box. With auto or scroll, the excess content is clipped to the element and reached by scrolling that element. A constrained nested div therefore has its own scroll position and visible window. overflow: hidden clips content without showing scrollbars, though it can still be scrolled programmatically.

Playwright documents page.screenshot({ fullPage: true }) as capturing the full scrollable page. That describes the page’s scrollable area; it is not an instruction to expand all nested scroll containers. A 2022 Playwright issue report describes a case where a non-body scrollable element’s contents were omitted even though body-scrolling content was captured. That report is one reproduction, not a guarantee about every browser or current Playwright version.

The pixels missing from the screenshot do not prove the content is absent. It may still be in the DOM and reachable by scrolling the inner panel.

2. Diagnose which element scrolls

Inspect the rendered element rather than guessing from the page’s appearance. Look for height, max-height, block-size, or max-block-size together with overflow-y: auto, scroll, or hidden. Then compare the panel’s scroll dimensions with its visible dimensions.

const diagnosis = await page.locator('#results').evaluate((el) => {
  const style = getComputedStyle(el);
  return {
    overflowY: style.overflowY,
    height: style.height,
    maxHeight: style.maxHeight,
    blockSize: style.blockSize,
    clientHeight: el.clientHeight,
    scrollHeight: el.scrollHeight,
    scrollTop: el.scrollTop,
    documentScrollHeight: document.documentElement.scrollHeight,
  };
});
console.log(diagnosis);

If scrollHeight is greater than clientHeight, the element has more vertical content than its visible box. To verify behavior, scroll the page and the candidate div separately and observe which one moves the omitted rows into view. If the visible wrapper is not the scroller, inspect its descendants. The actual scrolling region may be inside an iframe or Shadow DOM.

3. Choose a capture method

Content and goal Method Watch for
Static panel; capture the whole page Temporarily remove the panel’s height limit and expose overflow. Reflow, sticky elements, and fixed-position elements may move or overlap.
Static panel; capture only the panel Expand it, then take an element screenshot. An element screenshot alone may still show only the element’s rendered bounds, not all its clipped overflow.
Lazy-loaded or changing panel Scroll the panel incrementally, wait for content, and capture overlapping views for stitching. Wait for each new row or image to render; use overlap to account for sticky headers and variable-height content.
Virtualized list Scroll through the list and capture each rendered range. Virtualized lists may recycle DOM nodes. Increasing the panel’s height alone may not reveal the complete logical dataset.
The document should scroll instead Revisit the fixed height and overflow rules in the page’s layout. Nested scrolling may be an intentional design choice, especially on small screens.

4. Capture a static overflow panel with Playwright

This runnable Node.js example expands a static panel, waits for layout, captures the full page, and restores the element’s original inline styles. Change the URL and selector to match the page. If the panel’s original rules come from stylesheets, setting inline overrides is sufficient for this capture, while restoring the saved style attribute returns the original inline state.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });

try {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  const panel = page.locator('#results');
  await panel.waitFor({ state: 'visible' });

  const originalStyle = await panel.getAttribute('style');
  try {
    await panel.evaluate((el) => {
      el.style.setProperty('height', 'auto', 'important');
      el.style.setProperty('max-height', 'none', 'important');
      el.style.setProperty('block-size', 'auto', 'important');
      el.style.setProperty('max-block-size', 'none', 'important');
      el.style.setProperty('overflow', 'visible', 'important');
      el.style.setProperty('overflow-y', 'visible', 'important');
    });

    await page.evaluate(() => new Promise((resolve) => {
      requestAnimationFrame(() => requestAnimationFrame(resolve));
    }));
    await page.screenshot({ path: 'full-page.png', fullPage: true });
  } finally {
    if (originalStyle === null) {
      await panel.evaluate((el) => el.removeAttribute('style'));
    } else {
      await panel.evaluate((el, style) => el.setAttribute('style', style), originalStyle);
    }
  }
} finally {
  await browser.close();
}

Use an isolated page or browser context when practical so the temporary CSS cannot affect a later capture. Inspect the output: expanding a panel can change document height, trigger reflow, alter sticky and fixed positioning, and produce a result different from the interface a user sees. If setting height: auto does not work, check whether a parent constrains the panel, whether a flex or grid layout imposes a size, or whether the content itself loads only when scrolled.

5. Capture a panel with dynamic or virtualized content

When rows, images, or other content appear as the user scrolls, expanding the container may not load them. Scroll the actual panel, wait for the content relevant to that position, and capture each visible region. Use overlapping captures so you can align them later. A simple loop for a panel with stable content can collect viewport-sized slices; site-specific waits are necessary for asynchronous rendering.

const panel = page.locator('#results');
const step = 600;
const total = await panel.evaluate((el) => el.scrollHeight);

for (let y = 0; y < total; y += step) {
  await panel.evaluate((el, top) => { el.scrollTop = top; }, y);
  await page.waitForTimeout(250); // Replace with a condition for your app when possible.
  await panel.screenshot({ path: `panel-${String(y).padStart(6, '0')}.png` });
}

This is a starting point, not a universal stitching solution. The final position may extend beyond the last full step, a sticky header may repeat in each image, content heights may change, and virtualized lists may reuse the same DOM nodes for different rows. Confirm continuity by checking row labels or another stable identifier. For an element-only deliverable, screenshot the panel after scrolling or expanding it as appropriate; targeting an element does not by itself guarantee capture of its clipped overflow.

6. Keep the screenshot’s purpose in view

There are two valid goals that can produce different images:

  • Document all available content: expand static content or capture and stitch successive scroll positions.
  • Show the interface as a user sees it: preserve the panel’s height and overflow, and capture the visible state. The omitted rows are outside that view by design.

Choose deliberately. Temporary CSS may change layout and sticky behavior; a stitched view may not correspond to one real interface state. For an archival or debugging capture, record which approach was used.

7. Troubleshooting

Symptom Likely cause What to do
Full-page output ends at the panel’s visible bottom. The panel is a nested scroll container; its hidden scroll extent is not document height. Check scrollHeight versus clientHeight; expand static content or capture the panel’s scroll positions.
The panel has no scrollbar but content is missing. overflow: hidden clips content without a visible scrollbar, or a parent clips it. Inspect computed overflow and ancestor dimensions; temporarily expose overflow if a complete capture is intended.
CSS override has no visible effect. A parent, flex/grid sizing rule, another constrained dimension, or a script may control the size. Inspect ancestors and computed styles; target the actual scroll container and check again after layout settles.
Rows are still missing after expanding the panel. Content may be lazy-loaded or the list may be virtualized. Scroll in steps, wait for rows or images, and capture each range. Increasing height cannot reveal DOM nodes that have not been rendered.
Capture looks different after expansion. Expansion caused reflow or changed sticky/fixed positioning. Use a panel-only capture, capture successive original-state views, or treat the changed layout as an intentional full-content rendering.
Bottom rows are cut off in stitched output. The last scroll offset or overlap did not cover the end, or content height changed during capture. Include the final scroll position explicitly and verify stable row continuity and the panel’s final dimensions.
The wrong element moves while scrolling. The visible wrapper is not the actual scroller; an iframe or Shadow DOM may contain it. Inspect descendants and the frame or shadow tree, then target the element whose scrollTop changes.

8. cURL, Python, and Node.js with ScreenshotNeo

If you already have a page that renders the content you want, you can request a screenshot through ScreenshotNeo. Its screenshot API has options for full-page capture and custom CSS and JavaScript. For an overflow div, use a capture-time CSS override to expand a static panel, or page JavaScript to scroll a dynamic panel; inspect the result because virtualized content may still require a deliberate multi-step workflow. See the ScreenshotNeo API documentation for the available parameters.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.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://example.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);

In the Node.js example, replace Bun.write with your preferred file-writing method if you are running Node rather than Bun:

import { writeFile } from 'node:fs/promises';
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

9. Performance, reliability, and cost

Expanding a large static panel can make the page taller and increase rendering and image-encoding work. Incremental capture limits each screenshot to a panel view, but adds browser interactions, waits, and image-stitching work. Lazy images, animations, changing data, and virtualized rows can make captures inconsistent unless the page is settled and the content is checked at each position.

For reliable results, use a stable test page state, wait for a selector or a known application condition instead of relying only on a fixed delay, and verify the first and last items in the output. Keep the original layout when visual fidelity is the goal. No universal timing or performance figure applies to every page.

ScreenshotNeo bills only clean shots: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses include X-Page-Verdict and X-Billed headers so you can see the result classification and billing status. Its plans are Free: 1,000 shots/month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. A workflow that captures multiple scroll positions may require multiple screenshot requests; plan for the number of captures your chosen approach uses.

10. Or skip the browser setup

ScreenshotNeo can return a screenshot with one API request. Cookie banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, use screenshot and page-info tools. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

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

See the API docs for capture options, then sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does fullPage: true mean every element is expanded?

No. It means the full scrollable page. Nested elements with their own overflow retain separate scroll regions unless you change or capture them explicitly.

Will overflow: hidden content appear in a screenshot?

Only if it is within the captured rendered region or you change the relevant clipping and dimensions. Hidden overflow can still be scrolled programmatically, but it does not automatically extend the page capture.

Can I simply screenshot the div?

You can target the div, but an element screenshot may capture its rendered bounds while its overflow remains clipped. Expand it or capture its scroll positions if you need all inner content.

Is expanding the panel always the best fix?

No. It works best for static content. For virtualized or user-triggered content, scroll-and-capture is usually more appropriate; for a faithful UI screenshot, keep the panel in its normal state.