ScreenshotNeo

BlogHow-to

Playwright Screenshot After Scrolling a Specific Page Container

Set a nested container’s scroll position with Playwright, then capture its visible contents with a locator screenshot. Includes runnable JavaScript, Python, and cURL options.

By the ScreenshotNeo team4 October 20268 min read

To screenshot a specific scrollable page container in Playwright, set that element’s own scrollTop, then call screenshot() on its locator. A locator screenshot captures the element’s bounds and the content currently visible inside it; it does not reveal the container’s entire hidden scroll area.

const container = page.getByTestId('scrolling-container');
await container.evaluate(el => { el.scrollTop = 400; });
await container.screenshot({ path: 'container.png' });

Replace the test ID and offset with values for your page. The example uses Playwright’s JavaScript API, which is also the API used in TypeScript. For a stable screenshot, wait until the page and the container’s content are ready before capturing.

1. Set the container position and capture it

Use a locator for the nested element that actually scrolls. Setting its scrollTop directly gives you a repeatable position:

import { test, expect } from '@playwright/test';

test('captures the results panel at a chosen position', async ({ page }) => {
  await page.goto('https://example.com/results');

  const container = page.getByTestId('scrolling-container');
  await expect(container).toBeVisible();
  await container.evaluate(el => { el.scrollTop = 400; });
  await container.screenshot({ path: 'container.png' });
});

This assumes your page has a uniquely identified, visible, scrollable element with data-testid="scrolling-container". If your project configures a different test ID attribute, use that configured value. The offset is in CSS pixels and is specific to your content and viewport.

Scroll by a relative amount

If the test should advance the container from its current position rather than set an exact position, increment its scroll offset:

await container.evaluate(el => { el.scrollTop += 100; });
await container.screenshot({ path: 'container.png' });

For user-like wheel input, move the pointer over the container and send a wheel event:

await container.hover();
await page.mouse.wheel(0, 100);
await container.screenshot({ path: 'container.png' });

Wheel input follows the page’s interaction behavior and can be useful when testing that behavior. The amount moved can depend on the browser and page, so use direct scrollTop assignment when the test requires a specific position.

Choose a stable locator

Prefer a locator tied to the user-facing role, label, text, or an explicit test contract such as a test ID. If the page has no suitable semantic locator, a CSS selector can work:

const container = page.locator('.results-panel');
await container.evaluate(el => { el.scrollTop = 400; });
await container.screenshot({ path: 'results-panel.png' });

That selector must identify the intended container uniquely. Long selectors based on incidental DOM nesting are more likely to break when markup changes. See the Playwright locator guidance.

2. Know what the screenshot includes

Capture call What it captures When to use it
container.screenshot() The matched element’s bounds and the content currently visible inside it. One panel, list, feed, or nested scroller.
page.screenshot() The current viewport by default. The visible page as it appears at the current viewport position.
page.screenshot({ fullPage: true }) The page’s full scrollable extent as a tall capture. A full-page image when the document itself scrolls.

A full-page screenshot is not a way to expand one nested container’s hidden contents. If you need the whole inner list, scroll and capture it in sections, or use a page-specific approach to assemble the desired output. Playwright documents the locator capture behavior in its Locator screenshot API and page capture behavior in the screenshot guide.

A locator screenshot automatically scrolls the target element into view before capturing it. That makes the element visible in the page viewport; it does not choose an internal scroll offset for a nested container. Set scrollTop yourself when you need a particular section. The locator screenshot waits for actionability and fails if the element is detached. If another element covers the target, the result may not look as expected.

3. Make the capture repeatable

  1. Wait for navigation and content. Navigate to the target page and wait for the container to appear. If its rows load asynchronously, wait for a meaningful row or ready state too.
  2. Set the viewport. Container size and visible content depend on viewport dimensions. Set a fixed viewport in the Playwright context or test configuration for consistent captures.
  3. Set the internal offset. Assign an explicit scrollTop after the container has content. If content is still loading, the maximum scroll position may change.
  4. Capture the locator. Use container.screenshot({ path: 'container.png' }) for the panel alone.
  5. Check the result. Inspect the output or assert that expected content is visible before capturing. There is no universal offset that selects the same logical item across different layouts.

Example with a content-ready condition:

const container = page.getByTestId('scrolling-container');
await page.goto('https://example.com/results');
await expect(container).toBeVisible();
await expect(container.getByText('Result 20')).toBeVisible();
await container.evaluate(el => { el.scrollTop = 400; });
await container.screenshot({ path: 'container.png' });

Use a readiness condition that fits the application. For virtualized lists, an item may not exist in the DOM until the list is scrolled; in that case, scroll first, then wait for the item expected at that position.

4. Other language bindings and command-line options

The title’s core example is JavaScript/TypeScript. The same DOM property approach works in Playwright’s Python binding. cURL does not control a local Playwright browser, but it can call a screenshot API when you want a hosted capture instead.

Python with Playwright

from pathlib import Path
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1280, "height": 800})
    page.goto("https://example.com/results", wait_until="domcontentloaded")

    container = page.get_by_test_id("scrolling-container")
    container.wait_for(state="visible")
    container.evaluate("el => { el.scrollTop = 400; }")
    container.screenshot(path="container.png")

    browser.close()

Install the Playwright Python package and its browser binaries as described in the official Python getting started guide. The example assumes Chromium is installed for Playwright.

cURL with ScreenshotNeo

A hosted screenshot service captures a URL; a plain URL request cannot set a nested element’s scroll position unless the service supports a corresponding interaction or script option. ScreenshotNeo supports custom JavaScript, but the exact API parameter for supplying it is documented in its API documentation. For a straightforward hosted page capture, this cURL request saves the returned image:

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

Node.js with ScreenshotNeo

This request likewise captures a URL through the ScreenshotNeo API; check the response before treating it as an image in production.

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/results'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

For the service’s full option names and response behavior, use the ScreenshotNeo API docs. A hosted request is a different capture workflow from controlling a locally launched Playwright page.

5. Troubleshooting

Symptom Likely cause Fix
The screenshot shows the top of the panel. The page scrolled the element into view, but its internal position was never changed, or application code reset it. Set scrollTop after the content is ready and immediately before the screenshot. Check the value with evaluate(el => el.scrollTop).
The intended item is missing. The chosen offset does not align with the item, or a virtualized list has not rendered it yet. Wait for content, scroll to the relevant location, then wait for the target item to appear. Prefer an item-based condition over assuming one offset works at every viewport.
The container locator times out. The selector is wrong, the element is not yet rendered, or it matches no visible element. Verify the locator against the page, wait for the application state that creates it, and ensure the selector identifies the correct container.
The capture throws because the element detached. A rerender replaced the element between locating and capturing. Wait for the UI to settle and resolve the locator again after the rerender. Avoid retaining an element handle across page updates.
The result is blank or obscured. An overlay covers the target, the page has not rendered, or the target is outside the expected state. Wait for the page’s ready condition and handle or dismiss the overlay if appropriate. Check whether another element covers the container.
Wheel input scrolls the page instead. The pointer is not over the container, or the container cannot consume the wheel delta. Hover the container first, check that it has overflow content, or set its scrollTop directly.
The offset is clamped or has no visible effect. The content is shorter than the container, so there is no scrollable overflow, or the value exceeds the maximum. Check scrollHeight, clientHeight, and scrollTop; wait for additional content if it loads asynchronously.

6. Performance, reliability, and cost

A single locator screenshot avoids capturing unrelated page regions and is usually the right output for one container. The main reliability risks are page state, changing content, overlays, font or image loading, and virtualized rendering. Fix the viewport, wait for the content you need, and set a deterministic position. If you need several positions, capture them sequentially and give each file a distinct name.

Playwright runs a browser that your application or CI environment must install and manage. A hosted screenshot API can reduce that browser setup for URL-based captures, but it has different controls and costs. ScreenshotNeo’s free plan includes 1,000 shots a month without a card. Paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan. Treat the price and included volume as plan facts, and check the product site for current details before choosing a plan.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. AI agents can use its MCP server tools: take_screenshot, get_page_info, and capture_pdf.

For a basic page capture, see the ScreenshotNeo API documentation and use:

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

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. The MCP server lets AI agents take screenshots. The free plan gives you 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Does a locator screenshot include the whole scrollable container?

No. It captures the element’s bounds and the content currently visible inside the scrollable element. Scroll to other positions and take additional captures if you need more of its contents.

Does fullPage: true capture a nested container’s full contents?

No. Full-page mode applies to the page’s scrollable extent. It does not expand a nested scroller into a tall image.

Why does Playwright scroll the container before capture?

The locator screenshot brings the target element into the viewport. That outer page movement is separate from setting the container’s own internal scrollTop.