How to Show All Rows of a Dynamic Table in a Screenshot Using Selenium with Python
Load and verify every table row, then capture the whole table or overlapping viewport images with Selenium Python.
Direct answer: driver.save_screenshot() captures the current browser window; it does not automatically include every row below the viewport. First make sure the rows you need have loaded. If the full table fits on screen, capture its element. If it is taller, scroll through it and save overlapping viewport screenshots. Check the final row or expected row count before treating the capture as complete.
The right wait condition and scroll target depend on the page. A table may use document scrolling, an inner scrollable container, pagination, lazy loading, or virtualization. The examples below handle document scrolling with rows that remain in the DOM and explain how to adapt the approach to other layouts.
1. Choose the capture method
| Table behavior | Approach | Important check |
|---|---|---|
| All rows fit in the viewport | Capture the table element | Confirm the element includes every required row |
| All rows are in the DOM, table is taller than viewport | Capture overlapping viewport segments while scrolling | Confirm the last segment includes the final row |
| Scrolling loads more rows | Scroll in steps and wait for row count or content to change | Stop only when the page-specific end condition is met |
| Rows are virtualized | Scroll and capture segments, or use the table’s export/data source | DOM row count may stay small because off-screen rows are removed |
| Table has its own scrollbar | Scroll the table container rather than the document | Check the container reaches its scroll bottom |
| Table is paginated | Capture each page or use the application’s export | A screenshot cannot include pages not currently rendered |
Selenium documents scrolling through JavaScript, current-window screenshots, and element screenshots in its Python documentation and WebDriver API reference.
2. Install Selenium and prepare the browser
Install Selenium in your Python environment:
python -m pip install selenium
Use a current Selenium release and an installed browser such as Chrome. Selenium Manager can handle driver setup for supported configurations. If your environment manages browser drivers separately, configure the driver according to that environment.
3. Capture all document-scrolled rows
This runnable example waits for a table, scrolls the document in viewport-sized increments, waits for the rendered row count to stop changing after each movement, and saves overlapping PNG segments. Set TABLE_SELECTOR to the table’s CSS selector and, when known, set EXPECTED_ROWS. It assumes the table’s rows remain in the DOM as the page scrolls.
from pathlib import Path
import time
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
URL = "https://example.com/report"
TABLE_SELECTOR = "table#results"
ROW_SELECTOR = f"{TABLE_SELECTOR} tbody tr"
OUTPUT_DIR = Path("table-shots")
EXPECTED_ROWS = None # Set an integer when you know the required row count.
WAIT_SECONDS = 20
MAX_STEPS = 200
OVERLAP_PX = 120
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
options = webdriver.ChromeOptions()
# Uncomment for a headless run:
# options.add_argument("--headless=new")
with webdriver.Chrome(options=options) as driver:
driver.set_window_size(1440, 1000)
driver.set_page_load_timeout(45)
driver.get(URL)
wait = WebDriverWait(driver, WAIT_SECONDS)
table = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, TABLE_SELECTOR)))
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)) > 0)
# Scroll the document. The small overlap helps avoid cutting rows at segment edges.
viewport = driver.execute_script("return window.innerHeight")
step = max(1, viewport - OVERLAP_PX)
previous_count = -1
stable_rounds = 0
for index in range(MAX_STEPS):
row_count = len(driver.find_elements(By.CSS_SELECTOR, ROW_SELECTOR))
if row_count == previous_count:
stable_rounds += 1
else:
stable_rounds = 0
previous_count = row_count
if EXPECTED_ROWS is not None and row_count >= EXPECTED_ROWS:
break
at_bottom = driver.execute_script(
"return window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2"
)
if at_bottom and stable_rounds >= 2:
break
driver.execute_script("window.scrollBy(0, arguments[0])", step)
try:
# A count change is useful when scrolling causes additional rows to load.
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)) != row_count)
except TimeoutException:
# No count change can be normal when rows were already in the DOM.
time.sleep(0.25)
driver.execute_script("return document.readyState")
driver.save_screenshot(str(OUTPUT_DIR / f"segment-{index + 1:03}.png"))
final_count = len(driver.find_elements(By.CSS_SELECTOR, ROW_SELECTOR))
if EXPECTED_ROWS is not None and final_count < EXPECTED_ROWS:
raise RuntimeError(f"Expected {EXPECTED_ROWS} rows, found {final_count}")
# Check the final row and leave the browser at the bottom for visual inspection.
rows = driver.find_elements(By.CSS_SELECTOR, ROW_SELECTOR)
if rows:
driver.execute_script("arguments[0].scrollIntoView({block: 'end'});", rows[-1])
driver.save_screenshot(str(OUTPUT_DIR / "final-row.png"))
print(f"Found {final_count} rendered rows. Review {OUTPUT_DIR} for gaps or duplicates.")
The example records a segment after each scroll step; the final-row image is a separate verification aid. For a long table this creates multiple images, not one stitched image. The wait for a count change is a useful signal for append-on-scroll tables, but it is not a universal “page finished loading” signal. Replace it with the application’s loading indicator, API state, or known last-row condition when available.
4. Capture one table element when it fits
If the entire table is rendered and its dimensions are manageable, an element screenshot is simpler:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver, 20)
table = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "table#results")))
table.screenshot("table.png")
This captures the selected element, not rows that have not been rendered. Very tall elements can also exceed browser or image dimension limits. If the table does not fit, use segments or the browser-specific full-page capability documented for the exact browser and Selenium version; do not assume save_screenshot() expands the viewport.
5. Handle an inner scroll container
For a table inside a panel with its own scrollbar, identify the container selector in the page inspector. Scroll that element and check its scrollTop against scrollHeight - clientHeight. For example:
container = driver.find_element(By.CSS_SELECTOR, "div.table-scroll")
viewport_height = driver.execute_script("return arguments[0].clientHeight", container)
step = max(1, viewport_height - 100)
for index in range(200):
driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollTop + arguments[1]",
container,
step,
)
# Replace this pause with a wait for the app's loading signal when possible.
time.sleep(0.3)
driver.save_screenshot(f"table-shots/panel-{index + 1:03}.png")
at_bottom = driver.execute_script(
"return arguments[0].scrollTop + arguments[0].clientHeight >= arguments[0].scrollHeight - 2",
container,
)
if at_bottom:
break
In this pattern, container must be the actual scrolling element. A table wrapper that has no constrained height may not scroll; in that case scroll the document. Capture a segment only after the page has settled enough to avoid screenshots of loading placeholders.
6. Lazy loading, pagination, and virtualized rows
Lazy-loaded or infinite-scroll rows
Move down in increments rather than jumping straight to the bottom. Some pages load another batch only when a sentinel enters view. Wait for a row count increase, a loading indicator to disappear, or a known final-row label. Set a maximum step count so a broken or endless feed does not loop forever.
Virtualized tables
A virtualized table keeps only rows near the viewport in the DOM and reuses those elements as you scroll. In that case, the DOM row count is not the total dataset size, and capturing only the first viewport misses later rows. Scroll each segment and capture it after the visible row content changes. If you need a single complete artifact, prefer the application’s export or data endpoint; reconstructing a long table image from segments requires careful overlap and deduplication.
Pagination
Pagination means additional rows are on another page, not lower in the current scroll region. Navigate through every page and capture each one, or use an export feature when the goal is a complete dataset. Do not use “document bottom reached” as proof that all pages were captured.
7. Verify coverage and image quality
- Compare the captured row count with a known total when one is available.
- Inspect the first and last visible row in each segment; adjacent segments should overlap enough to make continuity clear.
- Check for repeated or missing rows, loading placeholders, sticky headers covering data, and clipped columns.
- Confirm whether hidden, filtered, or collapsed rows are supposed to be included. A screenshot reflects rendered page state.
- For an audit trail, retain the page URL, capture time, expected row count, and segment filenames alongside the images.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the visible rows appear | save_screenshot() captured the current window |
Scroll and save segments, or capture a fitting table element |
| Row count never grows | Rows were already loaded, the wrong selector is used, or the table is virtualized | Inspect the DOM while scrolling; wait on content changes or a page-specific loading signal |
| Scrolling does not move the table | The page uses a nested scroll container | Scroll the element whose scrollHeight exceeds its clientHeight |
| Capture stops before the last row | Fixed iteration limit, delayed loading, pagination, or an incorrect bottom test | Raise the justified limit, wait for the loader, inspect pagination, and verify the known final row |
| Rows repeat or disappear between captures | Insufficient overlap, changing data, or virtualized row reuse | Increase overlap and wait for visible row content to settle; identify segments by their row labels |
TimeoutException while waiting |
Selector did not match, table never appeared, or the app did not reach the expected state | Check the selector and frame context; wait for the correct state and handle login or error pages explicitly |
| Screenshot call returns false or file is absent | Invalid path or filesystem permissions | Create the output directory and use a writable path; Selenium’s save method returns a success boolean |
| Element screenshot is clipped or fails | Element is too tall, outside the viewport, or constrained by browser capture limits | Scroll the element into view, reduce the capture area, or save overlapping viewport segments |
9. Performance, reliability, and cost
Each browser scroll and image write adds time and disk usage. Use a sensible overlap: enough to identify continuity and avoid cutting rows, without recapturing most of every viewport. Wait on a meaningful condition instead of adding long fixed sleeps. Set page-load and explicit-wait timeouts, cap scrolling iterations, and fail clearly when a known expected row count is not reached.
Dynamic pages can change while being captured. If consistency matters, use a stable test dataset or freeze updates if the application supports it. Headless and headed browser rendering can differ in fonts, viewport size, and layout, so set a fixed window size and inspect representative output. Selenium screenshots are local browser artifacts; storage, execution time, and browser infrastructure are costs of the environment running the script.
10. FAQ
Can Selenium put an arbitrarily long table into one PNG?
Not with the portable current-window screenshot call alone. Capture segments or use a browser-specific full-page mechanism whose limits you have checked.
Does scrolling prove the table is complete?
No. A scroll can reach the bottom while more rows are on another page, still loading, or absent from a virtualized DOM. Verify against the page’s own completion signal or expected total.
Can I use this for a table behind a login?
Yes, if your Selenium session can reach the authorized page. Complete the required sign-in flow first, then wait for the table and its data to render.
Or skip the browser setup
For a standard website capture, ScreenshotNeo is a screenshot API and MCP server from Yorker Media. One request returns an image or PDF, and its API documentation lists the capture options. For a rendered dynamic table, confirm the page has loaded the rows you need before requesting its screenshot; an API capture cannot include rows the page has not rendered.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/report -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/report"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server lets Claude, Cursor, and other MCP clients use screenshot tools.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card required.


