Take a Full-Page Screenshot of a Page with Nested Scrolling Containers
Playwright’s full-page option captures the document, not every scrollable panel. Learn how to capture the page and nested content deliberately.
A full-page screenshot captures the document’s scrollable page; it does not automatically reveal all content inside every nested scrolling container. In Playwright, use page.screenshot({ fullPage: true }) for the document. Capture a panel separately if you want its visible area. To include all of a panel’s content, deliberately expose or scroll through that panel and capture its positions, then assemble the results if appropriate. Verify the output: sticky elements, lazy loading, dynamic content, fixed dimensions, and virtualized lists can affect what appears.
The key question is: which element owns the scroll? A page may scroll at the document level, inside a dashboard panel, or both. A document screenshot and a screenshot of one scrollable element are different captures. Playwright documents full-page capture as a screenshot of the full scrollable page, while its element screenshot documentation says a scrollable target shows only the content currently scrolled into view. See the Playwright screenshot documentation and Locator screenshot API.
1. Identify what you need to capture
Before writing code, choose the desired output:
- Whole document: capture the page from its top to its document bottom. Use
fullPage: true. - One panel as it currently appears: capture the panel element. This includes its visible scroll area, not necessarily its hidden overflow content.
- All content in a nested panel: make a separate plan for the panel. You can change its scroll state and capture multiple positions, or adapt the page so the content is exposed before capture. These are practical strategies, not a guarantee that every site or layout will behave the same way.
- Whole page plus all panel contents: treat this as multiple capture targets. A page-level full-page flag alone is not a promise to expand every nested scroller.
Inspect the page’s scroll behavior in the browser. If the document stays still while a panel moves, the panel owns that scroll. If both move, plan to capture both levels.
2. Capture the document with Playwright
Install Playwright and a browser, then save this as capture.mjs. It takes a page-level full-page screenshot and, when a selector is provided, a separate screenshot of that element’s current visible state.
import { chromium } from 'playwright';
const url = process.argv[2] ?? 'https://example.com';
const panelSelector = process.argv[3];
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });
await page.screenshot({ path: 'page-full.png', fullPage: true });
if (panelSelector) {
const panel = page.locator(panelSelector);
await panel.waitFor({ state: 'visible', timeout: 15_000 });
await panel.screenshot({ path: 'panel-visible.png' });
}
} finally {
await browser.close();
}
Run it with node capture.mjs https://example.com, or pass a panel selector as the second argument: node capture.mjs https://example.com '.activity-feed'. The panel screenshot represents that element’s current visible area. It does not by itself capture the whole scrollable contents of the panel.
3. Capture all content inside a nested scroller
There is no universal full-page switch that reliably unrolls every nested scrolling region. Common approaches are:
- Capture the panel at several scroll positions. Scroll the panel through its content, capture each visible state, and combine the images if a single artifact is required. Account for overlap and fixed elements when assembling.
- Expose the content before capture. If you control the application or can safely alter it for capture, remove the panel’s height or overflow constraint so its content is laid out visibly. This changes page layout and can affect sticky positioning, so compare the result with the original.
- Use the application’s own export or data interface. For virtualized lists, the DOM may contain only the visible rows. A screenshot cannot capture rows that the application has not rendered; use an export or load the rows before capturing.
A simple repeated-capture pattern for a known panel is below. It scrolls by approximately one panel viewport per image, waits briefly for rendering, and writes numbered files. Adjust the delay and step for the application. This is a starting point: it does not stitch images or guarantee complete coverage when content loads dynamically.
import { chromium } from 'playwright';
const url = 'https://example.com';
const selector = '.activity-feed';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded' });
const panel = page.locator(selector);
await panel.waitFor({ state: 'visible' });
const totalHeight = await panel.evaluate(el => el.scrollHeight);
const viewportHeight = await panel.evaluate(el => el.clientHeight);
let index = 0;
for (let top = 0; top < totalHeight; top += viewportHeight) {
await panel.evaluate((el, y) => { el.scrollTop = y; }, top);
await page.waitForTimeout(300);
await panel.screenshot({ path: `panel-${String(index).padStart(3, '0')}.png` });
index += 1;
}
} finally {
await browser.close();
}
This example measures the panel once. If scrolling loads more rows, recheck scrollHeight during the loop and stop only when the panel reaches the end and no more content appears. For overlapping captures, use a step smaller than clientHeight, then crop or stitch the overlap carefully. A sticky header inside the panel may repeat in every image.
4. Firefox DevTools for manual capture
Firefox offers a full-page screenshot workflow and element-oriented capture in the Inspector. Its Web Console :screenshot helper supports --fullpage for a page and --selector for a selected element. These options establish the target, but the documentation does not say that a selected scrollable node automatically reveals its overflow content. Inspect the result. See Firefox DevTools command-line interpreter documentation.
Use this route for occasional manual screenshots. For repeated captures, a script is easier to parameterize and rerun consistently.
5. Other client options
If you need a quick check from a terminal, cURL can fetch a screenshot from a service that accepts a URL and screenshot parameters. The exact options depend on that service. This ScreenshotNeo example captures the page as a WebP image; it is a page capture and does not imply automatic expansion of nested scroll containers.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python example:
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)
Node.js example:
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', new Uint8Array(await res.arrayBuffer()));
The Node.js snippet uses Bun’s file writer. With Node.js, write the response bytes using node:fs/promises:
import { writeFile } from 'node:fs/promises';
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 writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
See the ScreenshotNeo API documentation for request parameters and response details.
6. Rendering details that can change the result
Lazy-loaded images and content
Some pages load images or rows only when they approach the viewport. A screenshot taken immediately after navigation may omit them. Wait for a meaningful selector or scroll the relevant region to trigger loading, then confirm the content is present before saving.
Sticky and fixed elements
Headers, toolbars, and floating controls may remain pinned during scrolling. In a series of panel captures, they can appear repeatedly. Decide whether repeated controls are part of the intended output; if not, hide or alter them only in a capture-specific context.
Dynamic and virtualized content
Live feeds can change while being captured. Virtualized lists often render only nearby items, so increasing screenshot height will not necessarily reveal every record. Stabilize the data if possible, scroll through the list to render each region, or obtain the content through the application’s data or export path.
Capture dimensions
Viewport width affects responsive layout, line wrapping, and panel dimensions. Use a consistent viewport across runs. A narrower viewport can change which element scrolls and how much of a panel is visible.
7. Performance, reliability, and cost
- Runtime: a document capture is usually one screenshot operation after navigation. Capturing every panel position adds navigation waits and image writes; use the smallest step size that still gives reliable coverage.
- Memory: very tall full-page images can consume substantial memory. Capturing panel slices limits each image’s dimensions, though stitching them later also uses memory.
- Repeatability: fix viewport size, wait conditions, target selectors, and application data state. Prefer a selector or explicit ready condition when network-idle is unreliable due to ongoing requests.
- Reliability: take a second pass or inspect dimensions and expected content when the page is dynamic. Do not treat a successful screenshot call as proof that every nested item was rendered.
- Cost: local Playwright has no per-screenshot API charge, but consumes compute and maintenance time. Hosted screenshot APIs charge according to their plan and billing rules; check the service’s current documentation and inspect its response status.
8. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Panel screenshot contains only a few rows | The panel has overflow scrolling; element screenshots show its current visible content. | Capture the panel separately at multiple scroll positions, or expose its content before capture. |
| Full-page image omits content from an inner panel | The panel, not the document, owns that scroll. | Capture the document and panel as separate targets. Verify each result. |
| Some images or rows are missing | Lazy loading has not run, or the list is virtualized. | Scroll the relevant region, wait for content, and check whether offscreen items are actually rendered. |
Capture times out at networkidle |
The page keeps network connections open or polls continuously. | Wait for domcontentloaded plus a page-specific selector, or use an appropriate fixed delay. |
| Repeated headers appear in panel slices | A sticky or fixed element stays visible at each scroll position. | Account for overlap during stitching or hide the element in a capture-specific stylesheet if suitable. |
| Layout differs between runs | Viewport, data, fonts, timing, or responsive breakpoints changed. | Pin the viewport and stabilize page state; wait for fonts and the specific content you need. |
| Historical examples suggest full-page capture fails with non-body scrolling | A March 2022 report described this with Playwright 1.20.0. | Treat it as a dated report, not evidence of a current defect. Reproduce with your current version and verify scroll ownership. |
9. Or skip the browser setup
ScreenshotNeo takes screenshots through one API request. For a standard page shot:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. For nested scrollers, this one-call page capture does not replace a deliberate plan to capture inner panel contents.
Sign up free for 1,000 screenshots a month, with no card required.
10. FAQ
Does fullPage: true expand every scrollable div?
No. It requests a screenshot of the full scrollable document. Nested scrolling elements have their own scroll state and require separate handling.
Can I capture an entire panel in one element screenshot?
Do not assume so. Playwright’s locator screenshot documentation says that for a scrollable element only the currently scrolled content is visible in the screenshot.
What if the page itself scrolls inside a wrapper instead of the body?
Identify that wrapper as the document-like scroll owner and test the current Playwright behavior with your layout. A 2022 issue reported a limitation in Playwright 1.20.0; it does not establish the behavior of current versions.
Can Firefox capture a selected node?
Yes. Firefox documents Inspector node screenshots and the console’s selector option. Check the output to see how overflow content was treated.
Should I stitch multiple panel captures?
Only when one tall artifact is useful. Preserve overlap for alignment, watch for repeated sticky controls, and account for content that changes during capture.


