Selenium Screenshot After Scrolling Through a Dynamically Loaded Table
Load the rows you need, wait for the table to update, then choose a Selenium screenshot method that captures the intended content.
To screenshot a dynamically loaded table with Selenium, scroll the page or the table’s own scrollable container, wait until the rows you need have appeared, and only then capture the visible window, table element, or full document. A screenshot call does not fetch rows that have not loaded, and a normal WebDriver screenshot shows the current window rather than stitching together previous scroll positions.
1. Identify the scroll container and capture scope
First determine how the table behaves. It may load more rows when the document scrolls, when a fixed-height table panel scrolls, or when a particular row enters view. It may also virtualize rows, keeping only a small portion in the DOM. Those details determine both the scroll operation and the wait condition.
| Desired image | Capture method | Limit to account for |
|---|---|---|
| What is currently visible | WebDriver window screenshot | Only the current browser window is captured. |
| The table element | WebElement screenshot | The element must actually contain or render the rows you want; clipping and virtualization can affect the result. |
| The whole document in Firefox with Python | Firefox full-document screenshot method | This is a Firefox Python API. Check support in the Selenium version and browser you use. |
| A long or virtualized table | Scroll and load the intended rows first, then capture deliberately | Rows that have not been fetched or rendered cannot be assumed to appear. |
2. Wait for the table state you need
A completed page load does not guarantee that JavaScript-driven table content is ready. Selenium’s documentation explains that JavaScript can change a page after the browser’s readiness state, and recommends waiting for an application condition that matters to the script. Its guidance is explicit: “Do not mix implicit and explicit waits.” An implicit wait affects element-location calls globally; an explicit wait polls for a selected condition. Combining them can make timing difficult to predict. See Selenium’s Waiting Strategies.
Useful conditions include a row count increasing, a known target row becoming present, or a loading indicator disappearing. Re-query the rows after an update if the application redraws the table: previously found elements may no longer represent the current DOM. No single loop or termination condition works for every table; stop when the target row is loaded or the site indicates there is no more data.
3. Runnable Python example: scroll, wait, and save
This example assumes the document itself scrolls and each load adds rows under table#results tbody tr. Replace the URL and selectors with those for your page. It scrolls the last currently loaded row into view, waits for a larger row count, and repeats until the configured target count is reached or a load produces no new rows. Set a sensible target and iteration limit for the application.
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
URL = "https://example.com/results"
ROW_SELECTOR = "table#results tbody tr"
TARGET_ROWS = 200
MAX_LOADS = 30
WAIT_SECONDS = 15
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, WAIT_SECONDS)
def rows():
return driver.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)
try:
driver.implicitly_wait(0) # Use explicit waits for this workflow.
driver.get(URL)
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)) > 0)
stalled_loads = 0
for _ in range(MAX_LOADS):
current = rows()
count_before = len(current)
if count_before >= TARGET_ROWS:
break
# Re-query on every iteration: the table may be redrawn after loading.
driver.execute_script("arguments[0].scrollIntoView({block: 'end'});", current[-1])
try:
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)) > count_before)
stalled_loads = 0
except TimeoutException:
stalled_loads += 1
if stalled_loads >= 2:
break
# Put the intended ending rows on screen before taking a window screenshot.
final_rows = rows()
if final_rows:
driver.execute_script("arguments[0].scrollIntoView({block: 'end'});", final_rows[-1])
# Choose the capture scope that matches the desired output.
driver.save_screenshot("table-window.png")
# For an element image instead, use:
# driver.find_element(By.CSS_SELECTOR, "table#results").screenshot("table.png")
print(f"Rows present at capture: {len(rows())}")
finally:
driver.quit()
The sample’s two unchanged waits are a site-specific stopping heuristic, not a universal signal that the table has ended. If the site exposes a “load more” button or an end marker, use that explicit state instead. If the app replaces the table after each request, reacquire the table and rows inside each wait rather than retaining old WebElement references.
4. Adapt the scrolling step
When the page scrolls
Scrolling the last loaded row into view is useful for infinite-scroll pages because it brings the loading boundary into the viewport. Alternatively, scroll by a measured increment with JavaScript, then wait for the row count or another observable state to change. Do not assume one large jump triggers every intermediate loading boundary.
When a nested panel scrolls
If the table sits in a fixed-height panel, scrolling window may do nothing. Find the panel that owns the scrollbar and scroll it, for example:
panel = driver.find_element(By.CSS_SELECTOR, "div.table-scroll-panel")
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", panel)
# Then wait for a site-specific change, such as a larger row count.
For incremental loads, advance the panel in smaller steps and wait after each step. Verify that the panel’s scroll position changes and that the expected loading signal responds.
When rows are virtualized
A virtualized table may render only the rows near the viewport and reuse or replace DOM elements as you scroll. In that case, the DOM row count may stay constant even though new data is being shown. Wait for an identifying cell value, a changed last-row key, or another application-specific signal instead of waiting for the count to increase. A single element screenshot may show only the currently rendered portion. If the objective is a complete record, exporting the table’s underlying data may be more reliable than trying to represent a virtualized view as one image.
5. Choose and take the screenshot
Selenium documents screenshots of the browsing context and screenshots of individual elements. The Python WebDriver reference describes get_screenshot_as_file as saving a PNG of the current window; the interaction documentation demonstrates element screenshots. For Firefox’s Python binding, the API reference documents save_full_page_screenshot and full-page PNG methods. See the official Python WebDriver API, Firefox WebDriver API, and Selenium window and tab interactions.
For an element screenshot in Python:
table = driver.find_element(By.CSS_SELECTOR, "table#results")
table.screenshot("table.png")
For a full-document capture with Firefox Python, after confirming your installed API supports it:
driver.save_full_page_screenshot("full-document.png")
A full-document method changes capture scope; it does not cause lazy-loaded or virtualized rows to be fetched. Ensure the intended content has appeared before capturing. The cited full-document method is specific to Firefox Python; verify the matching API for your Selenium version and browser.
6. cURL, Python, and Node.js alternatives for screenshot capture
These examples show how to request a screenshot of a page with a screenshot API. They do not scroll an authenticated, interactive Selenium session or trigger a site’s table-loading behavior. Use them when the target page is already accessible to the capture service and its default page rendering is sufficient.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/results -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/results"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
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())));
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; see the API documentation. For example:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/results -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. These reasons matter when a normal page capture is enough; a dynamically loaded or authenticated table may still need the Selenium interaction steps above.
Sign up for 1,000 free screenshots a month, with no card required.
8. Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| Screenshot has no rows or too few rows | Capture ran after document readiness but before the table’s JavaScript update, or scrolling did not reach the load boundary. | Wait for a row, target value, or changed count. Scroll the actual container and repeat until the target condition is met. |
| Wait times out although new data appeared | The table is virtualized, so rendered row count stays constant, or the selected selector does not match the updated rows. | Wait for a cell value, last-row identifier, loading indicator, or another signal that changes for this site. |
| StaleElementReferenceException | The app redrew the table and invalidated stored row references. | Locate rows again after each update and inside wait predicates; avoid holding a row element across a redraw. |
| Scrolling has no effect | The scrollable area is a nested panel, or the chosen element is not the active scrolling boundary. | Inspect which element owns the scrollbar, scroll that element, and confirm its scroll position changes. |
| Only the viewport appears in the image | A normal WebDriver screenshot captures the current window. | Use an element screenshot for the table or a documented full-document method supported by your browser and binding. |
| Full-page image still omits rows | The rows were lazy-loaded or virtualized and never appeared before capture. | Perform the required scrolling and wait for the data first; full-document capture alone does not fetch it. |
| Rows are clipped or text is tiny | The table exceeds the image or viewport dimensions, or its layout scales content. | Check the element bounds and screenshot scope. Consider a larger viewport, or capture sections separately if one legible image is impractical. |
| Wait duration behaves unpredictably | Implicit and explicit waits are both enabled, or the wait condition does not reflect the app state. | Use a zero implicit wait for this workflow and targeted explicit waits, as Selenium advises. |
9. Performance, reliability, and cost
- Performance: Scroll only as far and as often as needed. Waiting on a precise state avoids wasting time on long fixed sleeps, while a bounded loop prevents a broken infinite-scroll page from running forever.
- Reliability: Select a stable row selector and a meaningful completion condition. Re-query after redraws, distinguish end-of-data from a slow response, and inspect the saved image for missing rows, sticky headers, overlays, and clipping.
- Repeatability: Keep the browser, Selenium version, viewport, and capture scope consistent between runs. Browser-specific full-page screenshot support can differ.
- Cost: A local Selenium run uses your browser and infrastructure; there is no screenshot API request in the example. If using ScreenshotNeo, the stated plans are Free for 1,000 shots/month, 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. Only clean shots are billed; cache hits and failed or unsuitable captures cost nothing.
10. FAQ
Why does Selenium miss rows after the page loads?
Document readiness and application data readiness are different. Wait for the table condition your task needs, then capture.
Can a regular Selenium screenshot combine multiple scroll positions?
No. The ordinary WebDriver screenshot records the current window. Scroll-loaded segments are not stitched together automatically.
Does a full-page screenshot load all table data?
No. It captures document content supported by that API; the page still needs to fetch and render the desired rows first.
Should I use a fixed sleep?
A short delay can be useful for a known rendering transition, but it should not be the only signal for dynamic data. Prefer an explicit wait for an observable application condition.


