ScreenshotNeo

BlogHow-to

How to Take a Screenshot of a Nested Scroll Area in a React Web App

Capture the visible part of a nested scroll area with Playwright, or export it from React with html2canvas. Learn how to handle the full overflow content, virtualization, and common failures.

By the ScreenshotNeo team4 October 20268 min read

To capture the currently visible part of a nested scroll area in a React app, use a Playwright locator and call locator.screenshot(). It captures the element’s displayed bounds; it does not automatically include content hidden inside the scroll box. For an in-app PNG export, html2canvas can render a DOM-derived image. To capture all overflow content, expose it temporarily or capture scroll positions in segments.

Playwright’s fullPage: true captures the document’s full scrollable page. It does not automatically expand every nested scrolling element. Decide first whether you need the visible viewport, the whole document, or all content inside one nested box.

1. Choose the capture target

Need Approach What to expect
Visible part of a scroll box in a browser test Playwright locator screenshot The element’s currently displayed region
Entire page document Playwright page screenshot with fullPage: true The document’s scrollable height; nested overflow is not guaranteed to expand
All content in one nested box Temporarily expose the content, or capture and stitch segments Requires handling layout changes, lazy loading, and possibly virtualization
PNG download initiated inside the app html2canvas with a React ref A DOM-derived canvas image, not a literal browser screenshot

2. Capture the visible nested area with Playwright

Use a stable selector such as a test ID. This example assumes Playwright’s test runner and a page where the React component is already rendered:

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

test('captures the visible results area', async ({ page }) => {
  await page.goto('http://localhost:3000');
  const area = page.getByTestId('results-scroll-area');
  await expect(area).toBeVisible();
  await area.screenshot({ path: 'results.png' });
});

The locator screenshot scrolls the element into the page viewport as needed and captures its displayed bounds. If you want a specific inner scroll position, set scrollTop before capture:

const area = page.getByTestId('results-scroll-area');
await area.evaluate(el => { el.scrollTop = 600; });
await area.screenshot({ path: 'results-middle.png' });

600 is only an example. Set a position that makes sense for the test data and container dimensions. Assert that expected rows are present before taking the screenshot. Playwright documents that a scrollable element screenshot shows only the content currently scrolled into view; locator-based screenshots are the recommended approach over the discouraged ElementHandle screenshot API. See the Playwright locator screenshot documentation and ElementHandle screenshot documentation.

Make test captures repeatable

  • Wait for the specific content or a stable test condition, not an arbitrary long delay where possible.
  • Disable or finish animations if they make captures vary between runs.
  • Wait for images and fonts that affect the expected result.
  • Set the container scroll position explicitly for screenshots of a particular region.
  • Account for sticky headers, overlays, and responsive layout at the configured viewport.

3. Capture the whole document page

For a page-level capture, use fullPage: true:

await page.screenshot({ path: 'page.png', fullPage: true });

This option targets the document’s scrollable page. It does not mean “capture every nested scroll container at its full content height.” If the nested box clips content with overflow: auto or overflow: scroll, the hidden part may remain hidden in the image. The distinction between document capture and inner scrolling is also described in this Playwright issue discussion.

4. Capture all content inside the nested box

There are two practical strategies. Choose based on whether changing the layout for a test capture is safe.

Option A: expose the content temporarily

For a non-virtualized list whose content is present in the DOM, temporarily remove the height limit and clipping, capture the element, then restore the original inline styles:

const area = page.getByTestId('results-scroll-area');

await area.evaluate(el => {
  const node = el;
  node.dataset.captureOriginalStyle = node.getAttribute('style') ?? '';
  node.style.height = `${node.scrollHeight}px`;
  node.style.maxHeight = 'none';
  node.style.overflow = 'visible';
});

await area.screenshot({ path: 'results-all.png' });

await area.evaluate(el => {
  const original = el.dataset.captureOriginalStyle;
  if (original) el.setAttribute('style', original);
  else el.removeAttribute('style');
  delete el.dataset.captureOriginalStyle;
});

This is a starting pattern, not a universal layout transformation. Grid and flex sizing, sticky descendants, nested scroll boxes, and CSS rules can affect the result. Prefer a capture-specific React state or class when you control the component, and restore state in a finally block if the surrounding test may fail before restoration. Check that the resulting element really contains the expected content.

Option B: capture multiple scroll positions

When expanding the box would disrupt the layout, capture it in overlapping segments and stitch them in a separate image-processing step. Scroll by slightly less than the viewport height so adjacent captures overlap. This helps align edges, but fixed headers, animations, fractional pixel offsets, lazy-loaded content, or content changing during capture can create seams or duplicated or missing rows. Capture stable data and verify each segment before stitching.

Check for virtualization

Virtualized lists render only the rows near the viewport and may replace them as you scroll. Increasing a container’s CSS height cannot reveal rows that React has not rendered. For a complete export, use the list’s data or rendering layer to render all required rows in a capture-specific view, or scroll through and capture successive positions. For visual regression tests, capturing a few meaningful viewport positions is often more stable than producing one very tall image.

5. Export a PNG from React with html2canvas

Use a ref to identify the element. This example assumes html2canvas is installed in your app:

import { useRef } from 'react';
import html2canvas from 'html2canvas';

export function ResultsPanel() {
  const areaRef = useRef(null);

  async function downloadArea() {
    const element = areaRef.current;
    if (!element) return;

    const canvas = await html2canvas(element, {
      scale: window.devicePixelRatio
    });

    const link = document.createElement('a');
    link.download = 'results.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
  }

  return (
    <section>
      <button type="button" onClick={downloadArea}>Download PNG</button>
      <div ref={areaRef} data-testid="results-scroll-area" className="scroll-area">
        {/* Your results rows */}
      </div>
    </section>
  );
}

For a container such as .scroll-area, the element’s rendered box may still represent only its visible dimensions. If your export must include all overflow, prepare a capture-specific element or layout that exposes all the content. The html2canvas documentation explains that the library builds an image from the DOM and supported styles rather than taking a direct browser screenshot. Its configuration options include scale and useCORS.

Cross-origin content and CSS support

Cross-origin images can taint a canvas and prevent exporting it. useCORS: true asks the browser to load images with CORS, but the remote server must also allow your origin. Cross-origin iframe contents are restricted by browser security. Unsupported or differently rendered CSS, fonts, SVG, canvas, and embedded media can also make the output differ from what the browser displays. Check the actual assets and styles your app uses.

6. ScreenshotNeo: capture a website without setting up a browser

If the target is a publicly reachable website and you need a screenshot without maintaining browser automation, ScreenshotNeo provides a screenshot API and MCP server. A remote page screenshot captures the page as served; it cannot access a private React component or application state that is not reachable through the URL. Use browser automation or an in-app export when you need authenticated test state or a particular nested scroll position.

Or skip the browser setup:

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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

Replace the example URL with the public page you want to capture. Read the ScreenshotNeo API documentation for authentication and capture parameters. Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, and failed loads are never billed, and cache hits cost nothing. Responses include page-verdict and billing headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, with no card required.

7. Troubleshooting

Symptom Likely cause Fix
Only a small part of the list appears The screenshot captured the scroll box’s visible viewport Expose the content temporarily or capture multiple scroll positions; check for virtualization
fullPage does not include hidden rows It targets the document, not the nested overflow area Use an element-specific strategy for the inner box
The capture starts at the wrong rows The container’s scroll position was not set or its content changed Set scrollTop explicitly and wait for expected rows before capture
Rows are absent even after increasing the height A virtualized list has not rendered those rows in the DOM Render the needed data in a capture-specific view or capture successive positions
html2canvas output differs from the page DOM rendering support, CSS, or media differs from browser surface rendering Check supported styles and assets; use browser automation when a literal browser rendering is required
PNG export fails for remote images Cross-origin restrictions taint the canvas Use useCORS: true and ensure the image server sends suitable CORS headers
Segments have seams or duplicate rows Overlap, fractional scroll offsets, sticky content, animations, or changing data Use stable data, overlap captures, and validate alignment before stitching
Screenshot changes between test runs Late content, fonts, images, animation, or responsive layout changes Wait for relevant assets and stable selectors, disable animation, and fix viewport and scroll position

8. Performance, reliability, and cost

  • Playwright: A visible element capture is generally a smaller artifact than an extremely tall full-content capture. Very tall images require more memory and can be slower to write and inspect. Segmenting limits per-image size but adds capture and stitching work.
  • html2canvas: Higher scale increases pixel dimensions and memory use. Device-pixel scaling can produce sharper exports, but test the result on long panels and large screens.
  • Reliability: For visual tests, control the data, viewport, scroll position, and loading state. For exports, handle missing refs and rejected rendering promises, and give users a useful error if cross-origin assets prevent download.
  • Cost: Playwright and html2canvas run in your own test or app environment; account for the browser or compute resources you operate. ScreenshotNeo’s free plan includes 1,000 shots per 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, and every feature is on every plan.

9. Frequently asked questions

Does Playwright’s locator screenshot change the container’s scroll position?

It may scroll the element into the page viewport so it can capture it. Set the nested element’s own scrollTop explicitly when a specific inner position matters.

Can I use html2canvas for an exact browser screenshot?

No. It reconstructs an image from DOM content and supported styles. Use browser automation when you need the browser’s rendered surface.

Can a remote screenshot API capture a React component that only exists after login?

Only if the page and required state are reachable to the capture request through supported access configuration. For app-specific state or a controlled test session, use Playwright or an in-app export.

References