How to Take a Screenshot of a Nested Scroll Container in Playwright
Capture the visible portion of a nested scroll container, move it to a precise offset, or save and stitch multiple screenshots to cover its full content.
Use a Playwright locator for the scrollable element, set its scrollTop to the position you need, then call locator.screenshot(). That captures the element’s bounds and the content currently visible inside them. It does not automatically capture all of the container’s overflow content.
const panel = page.getByTestId('scroll-panel');
await panel.evaluate((element) => {
element.scrollTop = 500;
});
await panel.screenshot({ path: 'panel.png' });
Replace 500 with the desired vertical offset. For a horizontal container, set scrollLeft. For all inner content, capture overlapping positions and stitch the images yourself. Playwright’s fullPage option applies to a page screenshot, not as a documented full-content shortcut for a nested locator.
1. How locator screenshots handle nested scrolling
locator.screenshot() captures the located element’s page area, clipped to its bounds. For a scrollable element, the capture shows only the portion currently visible through that element. Before taking the screenshot, Playwright may scroll the outer page to bring the element into view; this does not expose all of the inner element’s overflow content. See the Locator screenshot API.
Use page.screenshot({ fullPage: true }) when you need the full scrollable page. Use a locator screenshot when you need one element’s current visible region. They solve different capture problems; see the Playwright screenshots guide.
2. Runnable example: capture a chosen inner position
This complete JavaScript example assumes Playwright is installed in a project and the target page contains an element with data-testid="scroll-panel". Save it as nested-shot.js and run it with Node.js.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const panel = page.getByTestId('scroll-panel');
await panel.waitFor({ state: 'visible' });
const dimensions = await panel.evaluate((element) => ({
scrollHeight: element.scrollHeight,
clientHeight: element.clientHeight,
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth,
}));
const requestedTop = 500;
const maxTop = Math.max(0, dimensions.scrollHeight - dimensions.clientHeight);
const actualTop = Math.min(Math.max(0, requestedTop), maxTop);
await panel.evaluate((element, top) => {
element.scrollTop = top;
}, actualTop);
await panel.screenshot({ path: 'panel.png' });
console.log({ ...dimensions, requestedTop, actualTop });
} finally {
await browser.close();
}
})();
Replace https://example.com with the page under test and choose a locator that identifies the intended scroll container. The example clamps the requested offset to the container’s maximum scroll position, so a too-large offset does not silently produce a misleading assumption about what was captured.
Use a stable locator
Prefer a test ID or a locator based on user-facing semantics over a long CSS or XPath chain. Locators tied closely to DOM structure can break when the page markup changes. The Playwright locator guide discusses locator strategies and resilience.
Wait for the right content
Wait for the container to be visible and, when its contents load asynchronously, wait for a meaningful child or application-specific ready state before setting the offset. A fixed delay can help with a known animation, but it is usually less reliable than waiting for the condition the screenshot depends on.
3. Capture the whole inner list with multiple screenshots
There is no documented locator option equivalent to full-page capture for all of a nested element’s overflow content. Capture the visible viewport at several offsets. Use overlapping steps if you plan to stitch the images, and inspect the final offset because browsers clamp scrolling at the end.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const panel = page.getByTestId('scroll-panel');
await panel.waitFor({ state: 'visible' });
const { scrollHeight, clientHeight } = await panel.evaluate((element) => ({
scrollHeight: element.scrollHeight,
clientHeight: element.clientHeight,
}));
const maxTop = Math.max(0, scrollHeight - clientHeight);
const step = Math.max(1, Math.floor(clientHeight * 0.8));
const offsets = [];
for (let top = 0; top < maxTop; top += step) offsets.push(top);
offsets.push(maxTop);
const uniqueOffsets = [...new Set(offsets)];
for (const [index, top] of uniqueOffsets.entries()) {
await panel.evaluate((element, value) => {
element.scrollTop = value;
}, top);
await panel.screenshot({ path: `panel-${String(index + 1).padStart(3, '0')}.png` });
}
} finally {
await browser.close();
}
})();
The final maxTop capture ensures the bottom edge is included even when regular steps do not land exactly there. If the content is taller than one viewport, adjacent captures overlap by roughly 20 percent with this step size. Stitch them in order using an image-processing workflow that accounts for the overlap. Sticky headers, scroll-triggered animations, and content that changes height while scrolling can make automatic stitching inaccurate; for those pages, use application-specific handling and verify seams.
4. Use mouse wheel scrolling when interaction matters
Directly assigning scrollTop is convenient and precise. If the page responds to actual wheel input, hover the container and use mouse.wheel(). Wheel input can scroll an inner element when the pointer is over it, but event handling, nested scroll chaining, and browser behavior can affect which element moves. Check the resulting offset before capture.
const panel = page.getByTestId('scroll-panel');
await panel.hover();
await page.mouse.wheel(0, 400);
const top = await panel.evaluate((element) => element.scrollTop);
console.log('Current inner scrollTop:', top);
await panel.screenshot({ path: 'panel-after-wheel.png' });
For deterministic screenshots, set scrollTop directly. Use wheel input when reproducing a user interaction or when the application’s behavior depends on scrolling events.
5. Useful capture options and layout considerations
| Need | Approach | What it captures |
|---|---|---|
| One inner viewport | locator.screenshot() |
The element’s current visible area |
| Specific vertical position | Set scrollTop, then screenshot |
The visible slice at that offset |
| Specific horizontal position | Set scrollLeft, then screenshot |
The visible slice at that horizontal offset |
| Entire page | page.screenshot({ fullPage: true }) |
The page’s full scrollable area |
| All nested overflow content | Capture multiple offsets and stitch | Multiple visible slices; stitching is your responsibility |
Playwright’s locator screenshot API also provides screenshot options such as output path, image type, quality for JPEG, timeout, and animation handling. Consult the API reference for options supported by your installed Playwright version. Image dimensions follow the element’s rendered bounds and screenshot scale settings; ensure the panel is laid out at the intended size before capture.
For horizontal and vertical scrolling, calculate each maximum as scrollWidth - clientWidth and scrollHeight - clientHeight, respectively. A requested offset beyond the limit is clamped by the browser. If the container uses virtualized rows, only a subset of items may exist in the DOM at once: scroll through it and wait for each viewport’s content before capturing. A single DOM expansion or full-page capture will not necessarily materialize virtualized items.
6. Visual regression reproducibility
Keep the browser version, operating system, viewport, device scale, fonts, and rendering mode consistent between baseline and comparison runs. Playwright notes that rendering can differ across host OS, browser version, settings, hardware, power source, and headless mode; it recommends using the same environment for screenshots and comparisons. See Playwright visual comparisons.
Also make page state repeatable: use stable test data, wait for relevant content, set a known scroll offset, and avoid capturing during transitions. If a page has sticky elements within the panel or scroll-linked effects, they may appear differently at different offsets. For debugging, log the measured dimensions and actual final offset alongside each saved image.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the top despite setting an offset | The locator points to the wrong element, the element is not scrollable, or application code resets its position. | Read back scrollTop after assignment; inspect scrollHeight and clientHeight; confirm the locator matches the inner panel. |
| Only part of the list appears | Locator screenshots capture the visible element viewport only. | Capture successive offsets and stitch, or use a page-level full-page screenshot only if the required content belongs to the page’s scroll area. |
| Requested offset is not reached | The offset exceeds the scroll limit or is negative and gets clamped. | Compute maxTop = scrollHeight - clientHeight and clamp the requested value. |
| Screenshot times out | The element never becomes visible or page rendering is delayed. | Check that navigation completed, verify the locator, and wait for the application’s relevant ready state. Adjust the screenshot timeout only when the page legitimately needs longer. |
| Wrong nested element scrolls with the mouse | The pointer is outside the intended panel, or wheel events are handled by an ancestor. | Hover the target, inspect its scrollTop afterward, or assign scrollTop directly. |
| Stitched image has repeated or missing rows | Captures have no overlap, offsets were clamped, or content changed while scrolling. | Use overlapping offsets, include the exact bottom offset, and wait for each viewport to stabilize before saving. |
| Visual snapshot differs across machines | Browser or host rendering environment differs. | Run baseline and comparison captures in the same environment and keep viewport and browser versions aligned. |
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its API can return a screenshot or PDF from one GET request, and its documentation describes the request options. For example, this captures a page URL; it does not target a nested scroll container inside that page.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.
Sign up free for 1,000 screenshots a month, no card required.
9. FAQ
Can I screenshot an element that is outside the viewport?
Playwright can scroll the outer page to bring a locator into view before taking its screenshot. The inner container still shows its current scroll position.
Does fullPage: true work with a locator?
The documented full-page screenshot option is for a page screenshot. It is not a documented way to capture all overflow content of a nested locator.
Why use direct scrolling instead of a wheel event?
Direct scrollTop assignment gives precise positioning. Wheel input is useful when the application reacts specifically to user scroll events.


