Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Short answer: Playwright’s full_page=True captures the entire scrollable document, not every pixel inside a nested scrolling panel. For a panel, table, chat window, or menu with its own scrollbar, use a locator and either (1) scroll and stitch overlapping screenshots, or (2) temporarily expand the element and capture it. The right method depends on lazy loading, virtualized rows, sticky children, and whether changing the layout is acceptable.

First identify what actually scrolls

“Full screenshot” can describe two different targets:

  • The document: the browser page itself scrolls. Playwright can capture it directly with page.screenshot(full_page=True).
  • A nested element: a div, table wrapper, chat pane, drawer, or menu has its own overflow: auto or overflow: scroll. A normal locator screenshot captures only the element’s visible, currently scrolled area.

Check the page in DevTools: inspect the suspected element and look for a scrollable client area, then compare scrollHeight with clientHeight. If the document’s document.scrollingElement.scrollHeight is larger than the viewport, use the page method. If a child’s scrollHeight is larger than its clientHeight, use a nested-element strategy.

Capture a complete scrollable page

For a page-level scrollbar, Playwright’s documented full-page option is the simplest and most reliable approach:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900})
    page.goto("https://example.com", wait_until="networkidle")
    page.screenshot(path="full-page.png", full_page=True)
    browser.close()

The asynchronous form is equivalent:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(viewport={"width": 1440, "height": 900})
        await page.goto("https://example.com", wait_until="networkidle")
        await page.screenshot(path="full-page.png", full_page=True)
        await browser.close()

asyncio.run(main())

full_page=True extends the capture to the page’s full scrollable height. It does not automatically expand a child element whose own scrollbar hides content.

#1 Best Overall
Sale
Philips 24 Inch Computer Monitor FHD 100Hz VA VESA Flicker-Free, 241V8LB
  • CRISP CLARITY: This 23.8″ Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
  • INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
  • THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors
  • WORK SEAMLESSLY: This sleek monitor is virtually bezel-free on three sides, so the screen looks even bigger for the viewer. This minimalistic design also allows for seamless multi-monitor setups that enhance your workflow and boost productivity
  • A BETTER READING EXPERIENCE: For busy office workers, EasyRead mode provides a more paper-like experience for when viewing lengthy documents

Why a locator screenshot is not enough for a nested panel

This captures the element’s clipped bounds as currently rendered:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/dashboard")
    panel = page.locator(".results-panel")
    panel.screenshot(path="panel-visible.png")
    browser.close()

If .results-panel has a 400-pixel viewport and 4,000 pixels of content, the image normally contains the visible 400 pixels only. Locator screenshots support PNG, JPEG, and WebP; options such as scale change CSS-pixel versus device-pixel output, but do not reveal hidden scroll content.

Strategy 1: scroll, capture, and stitch

Scrolling preserves the page’s rendered layout and is usually the safer choice for complex interfaces. It also gives lazy-loaded content a chance to render. The example below captures overlapping strips, removes the duplicate overlap, and writes one PNG.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Philips 22 Inch Computer Monitor FHD 100Hz VA VESA Flicker-Free, 221V8LB
  • CRISP CLARITY: This 22 inch class (21.5″ viewable) Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
  • 100HZ FAST REFRESH RATE: 100Hz brings your favorite movies and video games to life. Stream, binge, and play effortlessly
  • SMOOTH ACTION WITH ADAPTIVE-SYNC: Adaptive-Sync technology ensures fluid action sequences and rapid response time. Every frame will be rendered smoothly with crystal clarity and without stutter
  • INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
  • THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors
from io import BytesIO
from PIL import Image
from playwright.sync_api import sync_playwright

URL = "https://example.com/dashboard"
SELECTOR = ".results-panel"
OUTPUT = "results-panel-full.png"
OVERLAP = 40

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900}, device_scale_factor=1)
    page.goto(URL, wait_until="domcontentloaded")
    panel = page.locator(SELECTOR)
    panel.wait_for(state="visible")

    # Read dimensions in CSS pixels and record the original position.
    metrics = panel.evaluate("""el => ({
        scrollHeight: el.scrollHeight,
        clientHeight: el.clientHeight,
        clientWidth: el.clientWidth,
        scrollTop: el.scrollTop
    })""")
    total = metrics["scrollHeight"]
    viewport = metrics["clientHeight"]
    step = max(1, viewport - OVERLAP)
    positions = list(range(0, max(1, total - viewport + 1), step))
    last = max(0, total - viewport)
    if positions[-1] != last:
        positions.append(last)

    strips = []
    for top in positions:
        panel.evaluate("(el, y) => { el.scrollTop = y; }", top)
        # Wait for scroll handlers, lazy content, and paint to settle.
        page.wait_for_timeout(150)
        panel.scroll_into_view_if_needed()
        strips.append(Image.open(BytesIO(panel.screenshot(type="png"))).convert("RGB"))

    width = max(image.width for image in strips)
    output_height = strips[0].height + sum(image.height - OVERLAP for image in strips[1:])
    result = Image.new("RGB", (width, output_height), "white")
    y = 0
    for index, image in enumerate(strips):
        crop = image if index == 0 else image.crop((0, OVERLAP, image.width, image.height))
        result.paste(crop, (0, y))
        y += crop.height
    result.save(OUTPUT)

    # Restore the user’s original scroll position.
    panel.evaluate("(el, y) => { el.scrollTop = y; }", metrics["scrollTop"])
    browser.close()

Install the image library with pip install playwright pillow, and install browser binaries with playwright install chromium. The stitcher assumes the panel’s width and height remain stable. If a scrollbar changes the available width, capture after the first scroll and keep the same dimensions for every strip.

Make stitching robust

  • Use overlap: a 20–80 pixel overlap helps avoid gaps caused by fractional scroll positions. The overlap must match the actual duplicate region; it is not a universal constant.
  • Use the final position explicitly: stepping by a fixed amount often misses the exact bottom. Always add scrollHeight - clientHeight.
  • Wait for content: after each scroll, wait for a known row or image, a short delay, or a page-specific loading indicator to disappear.
  • Freeze animations: inject temporary CSS that disables transitions and animations if moving elements create seams.
  • Watch sticky children: a sticky header may appear in every strip. Crop that repeated area or temporarily disable stickiness if visual fidelity permits.
  • Do not assume one scroll container: nested panels can require selecting the innermost element and separately handling an outer container.

Strategy 2: temporarily expand the element

For a static panel, you can remove its height constraint, set overflow: visible, and capture the resulting element. This is simpler than stitching, but it can reflow the page, alter sticky positioning, trigger different responsive rules, or expose content that is normally virtualized.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900})
    page.goto("https://example.com/dashboard", wait_until="networkidle")
    panel = page.locator(".results-panel")
    panel.wait_for(state="visible")

    original = panel.evaluate("""el => ({
        height: el.style.height,
        maxHeight: el.style.maxHeight,
        overflow: el.style.overflow,
        overflowY: el.style.overflowY
    })""")
    panel.evaluate("""el => {
        el.dataset.screenshotOriginalStyle = el.getAttribute('style') || '';
        el.style.height = 'auto';
        el.style.maxHeight = 'none';
        el.style.overflow = 'visible';
        el.style.overflowY = 'visible';
    }""")
    page.wait_for_timeout(100)
    panel.screenshot(path="panel-expanded.png", type="png")
    panel.evaluate("""el => {
        if (el.dataset.screenshotOriginalStyle) {
            el.setAttribute('style', el.dataset.screenshotOriginalStyle);
        } else {
            el.removeAttribute('style');
        }
        delete el.dataset.screenshotOriginalStyle;
    }""")
    browser.close()

Prefer the original application’s layout rules where possible. For example, changing only max-height may preserve width, while replacing the entire inline style can remove unrelated runtime styles. If the application uses a virtualized list, expansion may still render only the rows near the viewport; use scroll-and-stitch or load all records through the application’s own UI instead.

Lazy loading, virtualization, and dynamic content

Lazy-loaded images

Scroll through the element before the final capture so image observers fire. Wait for each image to report complete and a nonzero natural width, or wait for a page-specific loading state. Otherwise, the stitched image may contain placeholders.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #3
Sale
Dell 24 Monitor - SE2426H - 23.8-inch FHD (1920x1080) 144Hz 1ms Display, in-Plane Switching (IPS) Technology, AMD FreeSync™, TÜV 3-Star 2X HDMI, Tilt
  • Clear visuals. Fluid motion: A 144Hz refresh rate and 1ms MPRT deliver smooth, tear‑free motion across work, gaming, and streaming for clearer, more fluid viewing.
  • Eye comfort: TÜV Rheinland 3‑star* certification reduces harmful blue light while preserving stunning color quality without compromise. *TÜV Rheinland 3-star eye comfort certification.
  • Wide viewing angle: Get consistent views across a wide 178° /178° viewing angle.
  • In-Plane Switching (IPS): See excellent color accuracy and consistency across wide viewing angles with In-plane Switching (IPS) technology.
  • Ultra-thin bezels: Maximize your viewing experience with thin bezels.

Virtualized rows

Virtualized tables deliberately remove off-screen rows from the DOM. A single expanded screenshot cannot include nodes that do not exist. Scroll-and-stitch can capture each rendered window, but rows may be recycled and heights can change. A stable row identifier and a wait condition for the first and last visible row make validation easier.

Infinite scrolling

If scrolling loads more records, scrollHeight is not final at the first measurement. Continue scrolling until the loading indicator disappears and the height stops increasing for several checks, or until the UI reports an end-of-list state. Set a maximum number of passes to avoid an endless feed.

Fonts and animations

Use a deterministic browser context, wait for document.fonts.ready, and disable animations when pixel-level comparisons matter. Different font availability changes line wrapping and therefore every subsequent stitch boundary.

Rank #4
Samsung 27" Essential S3 (S36GD) Series FHD 1800R Curved Computer Monitor
  • CURVED FOR ENHANCED ENGAGEMENT: An immersive viewing experience with a curved monitor that wraps more closely around your field of vision; It creates a wider view, enhancing depth perception and minimizing peripheral distraction
  • SMOOTH PERFORMANCE FOR SEAMLESS CONTENT: Stay in the action when playing games, watching videos, or working on creative projects; The 100Hz refresh rate reduces lag and motion blur so you don't miss a thing in fast-paced moments¹
  • MORE GAMING POWER: Gain the edge with optimizable game settings; Color and image contrast can be adjusted to see scenes more vividly and spot enemies hiding in the dark; Game Mode adjusts any game to fill the screen so you can view every detail²
  • KEEP IT EASY ON THE EYES: Care for your eyes and stay comfortable, even during long sessions; Advanced eye comfort technology certified by TÜV reduces eye strain by minimizing blue light and reducing irritating screen flicker²
  • INCREASED VERSATILITY: Connect to more; Plug devices straight into your monitor for increased flexibility, making your computing environment even more convenient

Output format, scale, and dimensions

Use PNG for lossless text and UI, JPEG for smaller photographic captures, and WebP when your downstream system accepts it. Playwright’s type option controls the format when saving or returning bytes. scale="css" keeps one output pixel per CSS pixel; scale="device" uses device pixels and can produce larger images on high-DPI contexts. Neither setting captures additional scroll content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Very tall images can exceed image-viewer or downstream API limits. For reports, save one image per panel or emit a PDF instead of creating a single enormous bitmap. Keep the browser viewport and device scale fixed across runs if you compare screenshots.

Common failures and fixes

Symptom Likely cause Fix
Only the visible panel appears A locator screenshot clips to the element’s viewport. Use scroll-and-stitch or temporary expansion; full_page=True is for the document.
Bottom rows are missing The loop never captured the exact final scroll position. Add scrollHeight - clientHeight as the last position.
Blank or half-loaded images Lazy loading or network requests are still pending. Scroll to trigger loading and wait for a selector, image completion, or loading indicator.
Visible seams or duplicated headers Incorrect overlap, sticky content, or fractional scroll offsets. Increase or measure overlap, crop repeated sticky regions, and use integer positions.
Expanded capture changes the design Removing the height constraint caused reflow or changed sticky behavior. Use stitching, or apply a narrower style override and restore the original style afterward.
Rows repeat or disappear The list is virtualized. Capture each rendered window while validating row identifiers; expansion alone cannot materialize removed rows.
TimeoutError while locating the panel The selector is wrong, the element is inside an iframe, or it appears after a delayed action. Use a role or test-id locator, wait for the iframe and select its frame, and wait for the panel’s visible state.
Image is unexpectedly large Device-pixel scaling or a very tall scroll area. Use scale="css", reduce the viewport width only if the layout allows it, or split output.

Performance and reliability checklist

  • Reuse one browser and context for a batch of pages, but create an isolated context when cookies or viewport settings differ.
  • Prefer wait_until="domcontentloaded" plus targeted waits over a global network-idle wait on applications with analytics or long-lived connections.
  • Measure scrollHeight after each pass when content can grow; stop only after the height and loading state stabilize.
  • Capture at a fixed viewport, device scale, timezone, locale, and authentication state.
  • Restore scrollTop and inline styles in a finally-style cleanup path if later test steps use the same page.
  • Validate the final image dimensions and, for data-heavy panels, inspect the first and last expected row or label.
  • Keep screenshots out of memory when they are huge: save strips to temporary files or process them incrementally before assembling the final image.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request can return PNG, JPEG, WebP, or PDF. It can accept cookie and consent banners like a visitor, then remove more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether it was billed.

For a normal full-page website capture, see the ScreenshotNeo documentation and run:

Best Value
Sale
Sceptre New 22-Inch Gaming Monitor, FHD 1080p, Up to 144Hz, HDMI, DisplayPort, Built-in Speakers, Machine Black (E225W-FW144 Series, 2026)
  • 【INTEGRATED SPEAKERS】Whether you're at work or in the midst of an intense gaming session, our built-in speakers provide rich and seamless audio, all while keeping your desk clutter-free.
  • 【EASY ON THE EYES】 Protect your eyes and enhance your comfort with Blue-Light Shift technology. This feature reduces harmful blue light emissions from your screen, helping to alleviate eye strain during long hours of use and promoting healthier viewing habits.
  • 【WIDEN YOUR PERSPECTIVE】Our sleek minimal bezel design ensures undivided attention. The nearly bezel-free display seamlessly connects in a dual monitor arrangement, delivering an unobstructed view that lets you focus on more at once, completely distraction-free.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python is:

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()
open("shot.webp", "wb").write(r.content)

And in Node.js:

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(`${res.status} ${res.statusText}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', bytes);

For a nested application panel, ScreenshotNeo can target an element by CSS selector and also supports full-page capture with lazy images loaded, custom CSS and JavaScript, clicks before capture, waits for a selector, delay or network idle, hidden selectors, blocked requests or resource types, cookies, headers, user agents, authorization, timezone and geolocation. It also offers dark mode, device presets or custom viewports, retina scale, transparent backgrounds, resizing, chosen-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage and OpenAPI APIs, PDF controls, and parameter names used by other screenshot APIs.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can perform captures without you wiring Playwright into the agent. Free accounts include 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Choosing the right approach

Situation Recommended method Main trade-off
Document-level scrollbar Playwright full_page=True Does not include hidden content inside nested scroll containers.
Static, non-virtualized panel Temporary expansion Fast and simple, but layout and sticky behavior can change.
Lazy-loaded or interactive panel Scroll-and-stitch Preserves rendered behavior, but needs waits, overlap handling, and validation.
Virtualized or infinite list Scroll-and-stitch with row checks More engineering; the DOM may not contain all rows at once.

Frequently Asked Questions

Can I pass full_page=True to locator.screenshot()?

No. The documented full-page option belongs to the page screenshot operation. A locator screenshot is clipped to the element’s rendered bounds, so nested scroll content needs a custom strategy.

Will increasing the screenshot scale reveal more rows?

No. CSS/device scale changes pixel density only. It does not change the element’s scroll range or include hidden content.

Which strategy is best for a virtualized table?

Scroll-and-stitch is generally the practical option, with waits and checks for row identities. Temporarily expanding the container cannot capture rows that the application has removed from the DOM.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.