ScreenshotNeo

BlogHow-to

How to Capture a Full Selenium Element Screenshot Beyond Monitor Size

Selenium captures the visible part of an element. Learn how to scroll, tile, and stitch a tall element screenshot reliably, plus a simpler API option.

By the ScreenshotNeo team1 October 20268 min read

Short answer: Selenium’s element screenshot command captures the visible region inside an element’s bounding rectangle. It does not promise a single image containing every pixel of a tall element outside the viewport. To capture a larger element, scroll the container that owns the overflow, take overlapping screenshots, and stitch the tiles into one PNG. The stitching workflow is your code; it is not a built-in Selenium feature.

The W3C WebDriver specification defines the command this way: “The command takes a screenshot of the visible region encompassed by the bounding rectangle of an element.” Selenium bindings expose convenient PNG screenshot methods, but those methods do not change that documented behavior.

1. Decide what you actually need

There are three different jobs that are often called a “full element screenshot”:

Requirement Correct approach
One element that is taller than the viewport Scroll its owner and capture overlapping tiles, then stitch them.
An element inside a scrollable panel Scroll the nested panel, not the document.
The complete scrollable document Use a tool with documented full-page support, such as Playwright’s fullPage option, if changing tools is acceptable.

Increasing the browser window can expose more pixels, but it only helps when the content fits in the larger viewport. It is not a general solution for arbitrarily tall content.

2. Selenium workflow: scroll, capture, stitch

  1. Locate the target element.
  2. Find the nearest scrollable ancestor. A nested panel may own the overflow even when the page itself also scrolls.
  3. Scroll the target into view and record the container’s initial scroll position.
  4. Capture the visible element region as a PNG.
  5. Advance the container by less than one viewport so consecutive images overlap.
  6. Repeat until the element’s content has been covered.
  7. Join the images in order and inspect seams, sticky overlays, lazy content, and dynamic changes.

The following Python example uses Selenium and Pillow. It works for a tall element whose nearest scrollable ancestor is the document or a nested panel. It deliberately keeps an overlap between tiles and uses the measured scroll delta to place each tile.

from pathlib import Path
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options

URL = "https://example.com/page-with-a-tall-element"
SELECTOR = ".report"
OUT = Path("full-element.png")
STEP_RATIO = 0.80  # 20% overlap

options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)

def scroll_state(element):
    return driver.execute_script("""
    const el = arguments[0];
    function canScroll(node) {
      const s = getComputedStyle(node);
      return /(auto|scroll)/.test(s.overflowY) && node.scrollHeight > node.clientHeight;
    }
    let owner = el;
    while (owner && owner !== document.body && !canScroll(owner)) owner = owner.parentElement;
    const isDocument = !owner || owner === document.body;
    if (isDocument) {
      return {
        owner: null,
        top: window.scrollY,
        viewport: window.innerHeight,
        total: document.documentElement.scrollHeight,
        elementHeight: el.getBoundingClientRect().height
      };
    }
    return {
      owner,
      top: owner.scrollTop,
      viewport: owner.clientHeight,
      total: owner.scrollHeight,
      elementHeight: el.scrollHeight || el.getBoundingClientRect().height
    };
    """, element)

def scroll_to(element, top):
    driver.execute_script("""
    const el = arguments[0], top = arguments[1];
    function canScroll(node) {
      const s = getComputedStyle(node);
      return /(auto|scroll)/.test(s.overflowY) && node.scrollHeight > node.clientHeight;
    }
    let owner = el;
    while (owner && owner !== document.body && !canScroll(owner)) owner = owner.parentElement;
    if (!owner || owner === document.body) window.scrollTo(0, top);
    else owner.scrollTop = top;
    el.scrollIntoView({block: "nearest", inline: "nearest"});
    """, element, top)

def capture(element):
    return Image.open(BytesIO(element.screenshot_as_png)).convert("RGBA")

try:
    target = driver.find_element(By.CSS_SELECTOR, SELECTOR)
    first = scroll_state(target)
    max_top = max(0, first["total"] - first["viewport"])
    step = max(1, int(first["viewport"] * STEP_RATIO))
    tiles = []
    positions = []
    top = first["top"]

    while True:
        scroll_to(target, top)
        driver.execute_script("window.scrollBy(0, 0)")  # allow layout to settle
        state = scroll_state(target)
        tiles.append(capture(target))
        positions.append(state["top"])
        if state["top"] >= max_top:
            break
        next_top = min(max_top, state["top"] + step)
        if next_top == state["top"]:
            break
        top = next_top

    # Place each tile using the actual scroll movement. This avoids assuming
    # that every screenshot has exactly the same visible height.
    width = max(tile.width for tile in tiles)
    output_height = tiles[0].height
    for i in range(1, len(tiles)):
        delta = positions[i] - positions[i - 1]
        output_height += max(0, min(tiles[i].height, delta))
    result = Image.new("RGBA", (width, output_height), (255, 255, 255, 255))
    y = 0
    for i, tile in enumerate(tiles):
        if i == 0:
            result.alpha_composite(tile, (0, y))
            y += tile.height
        else:
            delta = positions[i] - positions[i - 1]
            crop_start = max(0, tile.height - delta)
            visible = tile.crop((0, crop_start, tile.width, tile.height))
            result.alpha_composite(visible, (0, y))
            y += visible.height
    result.crop((0, 0, width, y)).save(OUT)
    print(f"Saved {OUT}")
finally:
    driver.quit()

Install the dependencies with pip install selenium pillow and ensure a compatible Chrome/Chromedriver setup is available. For production capture, replace the example URL and selector, and add explicit waits for the page’s content.

When the target itself is the scroll container

Some layouts put overflow: auto directly on the element you want to capture. In that case, its scrollHeight is the content height while its clientHeight is the visible height. The same loop applies, but the screenshot may include only the element’s viewport. Confirm the first and last tiles visually before relying on the output.

Horizontal or two-dimensional content

For a wide canvas, tile in both axes. Record scrollLeft and scrollTop, move by less than the client width and height, and stitch each row before joining rows. Keep overlap in both directions so borders and text can be aligned.

3. Make the capture stable

  • Wait for layout: wait for a selector, fonts, images, and application data before the first tile.
  • Disable motion: inject CSS that sets transitions and animations to zero.
  • Handle lazy loading: scrolling each tile can trigger images; wait for them before taking the next screenshot.
  • Freeze changing data: timestamps, rotating ads, counters, and live feeds can create visible seams.
  • Hide sticky overlays: fixed headers, chat buttons, and consent dialogs may appear in every tile and duplicate after stitching.
  • Use a consistent viewport and device scale: changing window size or device pixel ratio between tiles changes geometry.
driver.execute_script("""
const style = document.createElement('style');
style.textContent = `*, *::before, *::after {
  animation: none !important;
  transition: none !important;
  caret-color: transparent !important;
}`;
document.head.appendChild(style);
""")

4. Common failure modes and fixes

Symptom Cause Fix
Only the visible portion is saved That is the documented element screenshot behavior. Use tiled scrolling and stitching.
Scrolling does not reveal more content You scrolled the document while a nested panel owns overflow. Find the nearest ancestor with scrollable overflow-y and change its scrollTop.
Tiles have duplicated bands Overlap was pasted twice or the scroll delta was estimated incorrectly. Record actual scroll offsets and crop by the measured delta.
Missing bands or gaps The page moved farther than expected, or a sticky header changed the visible rectangle. Use smaller steps, wait after scrolling, and inspect element rectangles after each move.
Lazy images are blank Images load only after entering the viewport. Wait for image completion after each scroll; capture again if dimensions change.
Element is not found Selector runs before the app renders or inside an iframe. Use an explicit wait and switch to the correct iframe before locating it.
StaleElementReferenceException The framework re-rendered the element. Re-locate the element after the render and restart the tile sequence.
Screenshot dimensions change Responsive breakpoints, browser chrome, or device scale changed. Set a fixed window size and device scale; do not resize during capture.
Text is unreadable The final bitmap is extremely tall or was downscaled. Capture at the required device scale and consider tiled files or a PDF for delivery.

5. Performance, reliability, and cost considerations

Capture time grows with the number of tiles. Smaller steps improve overlap and seam tolerance but require more browser screenshots. Larger steps are faster but leave less room to correct layout changes. A practical starting point is 70–85% of the scroll container’s client height, followed by visual validation.

Memory use also grows with the final bitmap dimensions. A very tall PNG can be expensive to hold in memory and may exceed downstream image limits. Write intermediate tiles to disk when needed, stitch rows incrementally, or produce a PDF when the output is intended for reading rather than pixel comparison.

Reliability depends on page behavior. Fixed headers, virtualized lists, animations, ads, network retries, and content that changes while scrolling can all invalidate a seam. Record the browser, driver, Selenium binding and version, viewport, device scale, selector, and scroll container with each capture so failures can be reproduced.

6. Playwright and full-page alternatives

Playwright documents a fullPage screenshot option for the complete scrollable page and separately documents element screenshots. That is a tool-specific feature; it does not change Selenium’s WebDriver semantics. If switching automation tools is possible and your target is the entire document, evaluate the documented full-page mode. If Selenium is required, the scroll-and-stitch method gives you control over nested panels and custom crop rules.

7. Or skip the browser setup

ScreenshotNeo provides a website screenshot API when you need a clean capture without maintaining Selenium, a browser driver, or a stitching pipeline. Its request accepts a URL and returns PNG, JPEG, WebP, or PDF. Use the ScreenshotNeo API documentation for the complete option list.

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}`);

Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports its page verdict and billing status in X-Page-Verdict and X-Billed headers. It also offers full-page capture with lazy images loaded, CSS-selector element capture, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, authorization, timezone and geolocation controls, caching, signed links, asynchronous jobs, bulk capture, PDF options, and an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.

8. FAQ

Can Selenium capture an arbitrarily tall element in one command?

No. The WebDriver command is defined for the visible region in the element’s bounding rectangle. Use tiles and stitching, or a tool with a documented full-page option.

Should I enlarge the browser window first?

It can reduce the number of tiles when the content fits in the larger viewport. It cannot capture unlimited height beyond the viewport.

Why is scrolling the page not enough?

A nested panel can have its own scroll position. Scroll the ancestor whose scrollHeight exceeds its clientHeight.

How much overlap should tiles have?

Start around 15–30% overlap, then adjust for sticky headers, lazy content, and layout shifts. Use measured scroll offsets rather than assuming a fixed screenshot height.

Is a stitched image always pixel-perfect?

No. Dynamic content, animations, virtualized lists, fixed overlays, and responsive layout changes can create seams. Freeze the page and inspect the result against the source.