How to Take a Screenshot of an Infinite-Scroll Page in Selenium Python
Load infinite-scroll content in bounded steps, wait for the page to settle, then save a Selenium screenshot. Includes nested panels, Firefox full-page capture, and fixes.
To screenshot an infinite-scroll page with Selenium Python, scroll in bounded steps, wait for new content to load, stop when a page-specific condition is met (or a safe limit is reached), then call driver.save_screenshot("page.png"). That method captures the current browser window, so it saves the visible viewport, not automatically the entire long document. Scroll-and-wait behavior is site-dependent: there is no universal signal that means an infinite list is finished.
1. Install Selenium and prepare a browser
Install Selenium in the Python environment that will run the script:
python -m pip install selenium
The example uses Chrome. Selenium’s current Python package can manage a compatible driver in supported setups; if your environment requires a separately installed browser driver, install and configure it according to that environment. Use a URL you are authorized to automate, and replace the example URL with the target page.
2. A bounded, runnable infinite-scroll screenshot
This baseline scrolls to the document bottom, gives asynchronous content time to arrive, and checks whether the document height changes. It requires three consecutive unchanged-height rounds before stopping and has a hard iteration cap. Those values are practical defaults to tune, not Selenium requirements.
import time
from selenium import webdriver
url = "https://example.com/infinite-list"
output_path = "infinite-scroll.png"
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
try:
driver.get(url)
previous_height = driver.execute_script(
"return document.documentElement.scrollHeight"
)
stable_rounds = 0
max_rounds = 30
for round_number in range(max_rounds):
driver.execute_script(
"window.scrollTo(0, document.documentElement.scrollHeight)"
)
time.sleep(1) # Prefer an explicit, page-specific wait when available.
new_height = driver.execute_script(
"return document.documentElement.scrollHeight"
)
if new_height == previous_height:
stable_rounds += 1
if stable_rounds >= 3:
break
else:
stable_rounds = 0
previous_height = new_height
else:
print(f"Reached the {max_rounds}-round safety limit")
if not driver.save_screenshot(output_path):
raise OSError(f"Selenium could not save {output_path}")
print(f"Saved {output_path}")
finally:
driver.quit()
execute_script runs JavaScript in the active window or frame. Selenium’s documented bottom-scroll pattern uses window.scrollTo; the code above reads the document element’s height and uses that as a simple progress signal. Selenium WebDriver Python API · Selenium FAQ
3. Make the stopping condition match the page
Document height is convenient, but it is only a proxy. Some pages append items and grow; others replace earlier items, virtualize the list, use a nested scrolling panel, or load only after a button or keypress. A robust script should identify the page’s actual signal for progress and completion where possible.
Wait for an item count to increase
If each result has a stable CSS selector, wait for the count to grow after scrolling. This avoids relying only on a fixed sleep. Set a per-wait timeout and an overall scroll limit so a stalled request cannot loop forever.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
item_selector = ".result-card" # Replace with the page's item selector.
wait = WebDriverWait(driver, 10)
previous_count = len(driver.find_elements(By.CSS_SELECTOR, item_selector))
for _ in range(30):
driver.execute_script(
"window.scrollTo(0, document.documentElement.scrollHeight)"
)
try:
wait.until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, item_selector))
> previous_count
)
except Exception:
# No new item appeared within the wait window; decide whether this
# means completion for this site or a load failure to investigate.
break
previous_count = len(driver.find_elements(By.CSS_SELECTOR, item_selector))
if not driver.save_screenshot("infinite-scroll.png"):
raise OSError("Screenshot save failed")
For production code, catch TimeoutException specifically rather than the broad exception shown in this compact example, and distinguish the site’s confirmed end-of-results marker from a timeout. If the page has a loading indicator, wait for it to disappear after each scroll. If there is an explicit “Load more” control, click it and wait for the item count or loading state to change.
Use a known end marker
When the page exposes a marker such as an end-of-results element, wait for that marker after loading each batch, or stop as soon as it appears. A site-specific marker is stronger evidence of completion than unchanged height. Inspect the page structure and confirm the marker means the list is complete; generic footer elements can appear before all data is loaded.
4. Handle nested scroll containers and lazy images
If only a panel scrolls, moving the window will not trigger that panel’s loader. Locate the scrollable element and update its scrollTop, checking its scrollHeight or the number of child items instead:
from selenium.webdriver.common.by import By
panel = driver.find_element(By.CSS_SELECTOR, ".results-panel")
for _ in range(30):
old_height = driver.execute_script(
"return arguments[0].scrollHeight", panel
)
driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollHeight", panel
)
time.sleep(1)
new_height = driver.execute_script(
"return arguments[0].scrollHeight", panel
)
if new_height == old_height:
break
panel = driver.find_element(By.CSS_SELECTOR, ".results-panel")
if not driver.save_screenshot("panel.png"):
raise OSError("Screenshot save failed")
Re-find the panel if the site replaces the element while loading; otherwise Selenium may raise a stale-element error. For lazy-loaded images, scrolling can trigger loading, but also wait until the relevant images have completed if image completeness matters. For example, wait on a selector and inspect each image’s complete property and natural width. Do not assume that a loaded list means every image has finished decoding.
5. Viewport screenshot versus full-document screenshot
driver.save_screenshot(path) (also available as get_screenshot_as_file(path)) saves the current browser window as PNG and returns a boolean indicating whether saving succeeded. It is a viewport screenshot; a long page’s off-screen content will not all appear in that one image.
Selenium’s Firefox Python API documents separate full-document methods, including save_full_page_screenshot and get_full_page_screenshot_as_png. These are browser-specific. Load the desired infinite-scroll content first, and confirm behavior with the Selenium and Firefox versions in your environment. A full-document capture cannot include items the page has not inserted yet. Selenium Firefox WebDriver API
Firefox full-page example
from selenium import webdriver
options = webdriver.FirefoxOptions()
driver = webdriver.Firefox(options=options)
try:
driver.get("https://example.com/infinite-list")
# Run your bounded scrolling and loading waits here first.
if not driver.save_full_page_screenshot("full-page.png"):
raise OSError("Firefox full-page screenshot save failed")
finally:
driver.quit()
If the selected Firefox/Selenium combination does not expose that method, use a supported version or save the viewport with save_screenshot. A viewport image and a full-document image have different dimensions and purposes; choose based on whether the reader needs the current visible state or one tall document image.
6. Tune waits, viewport, and browser options
- Wait strategy: A short fixed pause is easy but can be too short on a slow page and waste time on a fast one. Prefer a wait for new items, a loading indicator, a known selector, or an end marker.
- Scroll step: Scrolling directly to the bottom is simple. Some implementations need incremental movement to trigger intersection observers; use smaller pixel steps and wait between them if a single jump fails to load content.
- Viewport size: Set
--window-size=WIDTH,HEIGHTto make viewport output repeatable. Responsive layouts may load different content at different widths. - Headless mode: Add
options.add_argument("--headless")when a visible browser is not needed. Browser behavior can differ by version or environment, so validate the capture dimensions and page behavior. - Output path: Use a writable, explicit path ending in
.png. Check the boolean return value from the screenshot method. - Frames: If the content is inside an iframe, switch to that frame before locating or scrolling its elements. Return to the default content when the page-level screenshot or controls require it.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the first batch appears | The script scrolls once, waits too briefly, or the site needs a different trigger. | Use a bounded loop; wait for item count growth or the loading signal; try incremental scrolling or the page’s “Load more” control. |
| The loop stops while results remain | Height stayed fixed, content was replaced, a nested panel scrolls, or the wait signal is unrelated. | Track item count or a site-specific marker, and scroll the actual container. |
| The loop never stops | Height keeps changing, the page continuously adds content, or the chosen condition never becomes true. | Set a maximum number of rounds and total time; define a stopping rule such as a known end marker or target item count. |
| Screenshot omits most of the page | save_screenshot captures the current window viewport. |
Use Firefox’s full-page API where supported, or capture the visible viewport after loading the desired section. |
| Images are blank or incomplete | Lazy images were not brought into view or had not finished loading when capture ran. | Scroll through image regions and wait for image completion or a page-specific ready condition before saving. |
StaleElementReferenceException |
The site replaced a panel or item node during loading. | Re-locate the element after each load rather than retaining a reference to the old node. |
Screenshot method returns False |
The destination may not be writable or the path may be invalid. | Choose a writable absolute path, use a .png suffix, and check permissions and available disk space. |
| Content differs in headless mode | Responsive behavior, browser version, viewport, or site automation behavior differs. | Set a deliberate window size, compare with a headed run, and inspect browser console/network failures where available. |
8. Performance, reliability, and cost
Every scroll-and-wait round adds browser work and elapsed time. A fixed delay pays the full pause on every round; condition-based waits can continue as soon as the needed change occurs. Avoid loading an unlimited feed when only a known number of results are needed: set a target count, end marker, maximum rounds, and overall timeout. Keep the browser session alive until saving completes, and put driver.quit() in a finally block so failures do not leave browser processes behind.
Reliability depends on the target page’s loading behavior, network, browser, and wait condition. Record the final item count and whether the stop condition was an end marker, a timeout, or the safety cap; this makes incomplete captures distinguishable from successful completion. Selenium itself is browser automation software, so the relevant direct costs are the machine and runtime you provide and any infrastructure you choose. The source material provides no benchmark or universal runtime figure.
9. Or skip the browser setup
ScreenshotNeo takes a screenshot with one GET request and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters. For an ordinary page capture, the request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
These examples capture the specified URL; they do not reproduce Selenium’s repeated scrolling through an infinite feed. Use Selenium when the content must be loaded through page interaction and verify that the desired items are present before capture. ScreenshotNeo can simplify screenshot delivery: it accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture, with each step configurable. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
10. FAQ
Can Selenium take a screenshot of the whole infinite page in one call?
The ordinary screenshot call captures the current window. First load the content you need; then use a browser-specific full-document method where supported, such as the documented Firefox API, or capture the viewport.
How do I know the infinite scroll is finished?
Use the page’s own end marker or known result count when possible. Otherwise combine a no-progress condition with a maximum round or time limit and treat reaching the limit as an incomplete or uncertain result.
Why does a one-second pause sometimes fail?
Network and rendering times vary, and some sites need a different scroll target or user action. Wait for a meaningful page condition rather than relying on a fixed delay alone.
Does a full-page screenshot trigger loading of unseen items?
No. Trigger the page’s loading behavior first; screenshot APIs capture content that is available in the page, not results the site has not inserted.


