How to capture a website screenshot after scrolling through nested containers
Capture content inside nested scroll areas with Playwright by scrolling each container, waiting for loaded content, and capturing visible sections.
Short answer: Scroll the nested container itself, then capture its visible content. Playwright’s fullPage: true captures the full scrollable page, but it does not guarantee that every inner overflow: auto region will be expanded. If you need the entire inner record, capture overlapping positions and combine them, or expand the container when the page allows it.
Choose the capture that matches your goal
| Goal | Approach | What to expect |
|---|---|---|
| Capture the document from top to bottom | page.screenshot({ fullPage: true }) |
Captures the page’s full scrollable surface. It does not promise to expand independent nested scroll areas. |
| Capture what is currently visible in a nested region | Scroll its locator and use locator.screenshot() |
Captures the element bounds and the content currently visible inside the scroll area. |
| Capture all content in a long nested region | Scroll through it in overlapping steps and capture each state | Produces multiple images that can be stitched or retained as a sequence. |
Playwright’s element screenshot scrolls the element into view before capture. If the element is itself scrollable, the screenshot shows only its currently scrolled content. See the Playwright screenshots guide and ElementHandle screenshot documentation.
Capture a nested scroll container with Playwright
The following Node.js example uses Playwright’s test runner. Replace the URL and selectors with ones from your page. It checks which elements overflow, scrolls the target, waits for a site-specific readiness signal, and captures each overlapping segment.
import { test } from '@playwright/test';
test('capture a nested scroll area', async ({ page }) => {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const outer = page.locator('[data-testid="outer-scroll"]');
const inner = outer.locator('[data-testid="inner-scroll"]');
// Confirm the target is actually scrollable and inspect its dimensions.
const dimensions = await inner.evaluate(el => ({
clientHeight: el.clientHeight,
scrollHeight: el.scrollHeight,
scrollTop: el.scrollTop
}));
console.log(dimensions);
if (dimensions.scrollHeight <= dimensions.clientHeight) {
throw new Error('Target does not have overflowing content');
}
// Make the outer region reveal the inner target first.
await outer.evaluate(el => { el.scrollTop = 0; });
await inner.scrollIntoViewIfNeeded();
const step = Math.max(1, Math.floor(dimensions.clientHeight * 0.8));
let index = 0;
let previousTop = -1;
while (true) {
const top = await inner.evaluate(el => el.scrollTop);
if (top === previousTop) break;
previousTop = top;
// Replace this with a meaningful signal if the site loads content on scroll.
await page.waitForTimeout(250);
await inner.screenshot({ path: `nested-${index}.png` });
index += 1;
const atEnd = await inner.evaluate(el => {
if (el.scrollTop + el.clientHeight >= el.scrollHeight) return true;
el.scrollTop = Math.min(el.scrollTop + Math.floor(el.clientHeight * 0.8), el.scrollHeight);
return false;
});
if (atEnd) break;
await page.waitForTimeout(250);
}
});
The fixed delay is a placeholder, not a guarantee that network or application work has finished. Prefer waiting for an element, changed item count, or other page-specific condition after each scroll. If content is lazy-loaded, traverse in increments instead of jumping straight to the bottom.
Install and run
npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium
# Save the example as nested-scroll.spec.js
npx playwright test nested-scroll.spec.js
Playwright’s documented page-level full-page option is shown in its Page API. Use this when the document itself is the scrolling surface:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Find and scroll the right container
Pages can have multiple independent scroll areas: a sidebar, a dialog, a table, and a nested panel. Inspect likely candidates and their descendants. An element overflows vertically when scrollHeight > clientHeight; for horizontal overflow, compare scrollWidth with clientWidth.
const candidates = await page.locator('body *').evaluateAll(elements =>
elements
.map((el, index) => ({
index,
tag: el.tagName,
id: el.id,
className: typeof el.className === 'string' ? el.className : '',
clientHeight: el.clientHeight,
scrollHeight: el.scrollHeight,
clientWidth: el.clientWidth,
scrollWidth: el.scrollWidth,
overflowY: getComputedStyle(el).overflowY,
overflowX: getComputedStyle(el).overflowX
}))
.filter(el => el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)
);
console.log(candidates);
Use a stable selector such as a test ID, accessible role, or application-specific attribute. A positional selector can break when the page adds another panel. For nested areas, scroll outer ancestors enough to reveal the target, then scroll the target itself. If the target is inside a modal or another clipped ancestor, capturing it without first positioning those ancestors may produce a clipped or offscreen result.
Handle lazy loading and dynamic content
Some sites add rows or cards only when a container approaches its bottom. A single jump to the maximum scroll offset can skip the trigger or capture before new content appears. Instead:
- Scroll by a fraction of the visible height, leaving overlap between captures.
- Wait for a specific signal: a new row, a changed count, a loading indicator to disappear, or a known network response.
- Re-read
scrollHeightafter loading because the container may have grown. - Continue until the bottom is reached and a further scroll adds no content.
For infinite lists, define a stopping condition. Examples include reaching a known item count, seeing an end marker, or reaching a maximum capture duration. Without one, a page that continuously appends content can keep the capture loop running indefinitely.
Capture one tall image or several segments
Playwright does not document a one-call option that universally expands all nested scroll regions into one image. A sequence of overlapping screenshots is the dependable general workflow for a tall inner region. Stitching those images is a separate image-processing step; align by the overlap and watch for repeated sticky headers or animated content. If you can safely change the page layout, temporarily increasing the container height and restoring it afterward may allow one element capture, but this can reflow content and change what the page renders.
Choose the artifact based on use: one tall image is convenient for a record, while separate segments preserve the viewport-like appearance and avoid very large image dimensions. For visual comparison, keep the same scroll positions, viewport, device scale, and application state between runs.
Or skip the browser setup
ScreenshotNeo provides a one-request screenshot API. It captures a URL as an image or PDF; it does not accept Playwright commands to scroll an arbitrary nested container, so use the DIY workflow above when the inner scroll position must be controlled. For ordinary URL captures, 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,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers say the page verdict and billing status.
- An MCP server lets AI agents use
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Learn more about ScreenshotNeo, then create a free account.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot shows only the top part of the inner content | The element screenshot captures the currently visible scroll position. | Scroll the target container and capture each needed position. The Playwright element screenshot docs describe this behavior. |
fullPage: true still leaves inner content clipped |
The document and nested element have separate scroll surfaces. | Scroll the inner element explicitly. A historical Playwright issue reports a related limitation for content in a non-body element; treat it as a diagnostic clue, since behavior may depend on version and browser. |
| Only a few segments are captured | The loop reads dimensions before lazy content expands, or stops at the old bottom. | After each scroll and load wait, re-read scrollHeight and continue until the end marker or stable item count appears. |
| Segments repeat or have gaps | The scroll increment exceeds the visible area, or browser scroll snapping changes the requested offset. | Use an increment smaller than clientHeight, inspect actual scrollTop after each scroll, and retain overlap. |
| Capture is blank or the target is missing | The outer panel has not revealed the target, a selector matched the wrong element, or the page has not rendered. | Check the locator count and bounds, scroll ancestors into view, and wait for a page-specific ready condition. |
| Images or text shift between segments | Lazy loading, animation, sticky elements, or live updates changed the page state. | Wait for stable content, disable or hide animation where appropriate, and note that fixed overlays may appear in every segment. |
Performance and reliability
- Capture cost: Each segment requires a screenshot operation and image output. Larger overlaps improve continuity but create more captures and more data to store.
- Waits: Avoid a long fixed sleep after every scroll if the application exposes a reliable readiness signal. A condition-based wait can reduce needless delay while avoiding premature captures.
- Stability: Use a fixed viewport and browser version, wait for fonts and page-specific content, and keep the target state reproducible. Network idle alone may not mean an infinite-scrolling application has finished.
- Memory and dimensions: A very tall single image can be costly to process and inspect. Segments bound individual image size; avoid stitching into dimensions unsupported by your downstream tools.
- Retries: Retry transient navigation or loading failures with a limit, and record which segment failed. Re-running the entire capture may produce duplicate or inconsistent output if the page is live.
FAQ
Does an element screenshot scroll the element to its bottom?
No. It brings the element into view, but a scrollable element still shows its current inner scroll position.
Can I capture nested horizontal scrolling too?
Yes. Set scrollLeft in controlled increments, inspect scrollWidth and clientWidth, and capture overlapping horizontal segments. For a two-dimensional region, iterate rows and columns.
Should I use mouse-wheel events or set scrollTop?
Setting scrollTop is direct and useful for deterministic positioning. Use wheel or keyboard input when the application only responds to user-like events or when you need to exercise its interaction behavior.
Does ScreenshotNeo expose a nested-container scroll option?
The supplied ScreenshotNeo options do not include a command to scroll an arbitrary page container to a chosen offset. Use browser automation for that specific workflow.


