How to Capture a Screenshot of an Infinite-Scroll Page with Selenium Java
Load an infinite-scroll page in steps, wait for new content, and capture the rendered result with Selenium Java. Includes runnable code and troubleshooting.
To capture an infinite-scroll page with Selenium Java, first scroll until the content you need has loaded, waiting on a signal specific to that site. Then capture the browser’s current viewport with Selenium’s TakesScreenshot API. A screenshot call does not load the rest of the feed, and a viewport screenshot is not automatically a full-page image.
This guide shows a runnable loop that waits for the number of rendered items to grow, stops when it reaches a configured target or a page-specific end marker appears, and saves a screenshot. You must adapt the CSS selectors and stopping condition to the site you are capturing.
1. Set up Selenium Java
This example uses Maven and Chrome. Selenium Manager can manage the browser driver for a standard local setup. Use a Java and Selenium version supported by your project and browser.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.27.0</version>
</dependency>
</dependencies>
The code below assumes the target page renders each feed entry with CSS class .feed-item. Replace that selector, the URL, and optionally the end-marker selector with values from the page you own or are authorized to access.
2. Runnable Java example: scroll, wait, capture
This version scrolls by roughly one viewport, waits until the item count increases or the page’s end marker becomes visible, and stops after a set number of rounds or when no more entries load. It saves a viewport screenshot showing the browser’s current scroll position.
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class InfiniteScrollScreenshot {
public static void main(String[] args) throws Exception {
String url = "https://example.com/feed"; // Replace with the page to capture.
By itemSelector = By.cssSelector(".feed-item"); // Replace with the repeated item selector.
By endMarker = By.cssSelector(".feed-end"); // Optional; remove if the page has no end marker.
int targetItems = 100; // Maximum desired loaded items; tune for the page.
int maxRounds = 40; // Safety bound against endless feeds.
Duration perRoundTimeout = Duration.ofSeconds(10);
Path output = Path.of("infinite-scroll.png");
ChromeOptions options = new ChromeOptions();
// For headless capture, uncomment the next line. Set a window size for a stable viewport.
// options.addArguments("--headless=new", "--window-size=1440,1000");
WebDriver driver = new ChromeDriver(options);
try {
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(45));
driver.get(url);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
JavascriptExecutor js = (JavascriptExecutor) driver;
// Wait for the first feed entry, or fail clearly if the selector is wrong/page did not load.
wait.until(d -> !d.findElements(itemSelector).isEmpty());
int previousCount = driver.findElements(itemSelector).size();
int stagnantRounds = 0;
for (int round = 0; round < maxRounds && previousCount < targetItems; round++) {
if (!driver.findElements(endMarker).isEmpty()
&& driver.findElements(endMarker).get(0).isDisplayed()) {
break;
}
long oldHeight = ((Number) js.executeScript(
"return document.documentElement.scrollHeight")).longValue();
js.executeScript("window.scrollTo(0, arguments[0])",
js.executeScript("return window.scrollY + window.innerHeight * 0.8"));
final int countBeforeScroll = previousCount;
try {
new WebDriverWait(driver, perRoundTimeout).until(d ->
d.findElements(itemSelector).size() > countBeforeScroll
|| (!d.findElements(endMarker).isEmpty()
&& d.findElements(endMarker).get(0).isDisplayed()));
} catch (org.openqa.selenium.TimeoutException noGrowth) {
// Some sites append items only after a delay or use a different signal.
// A timeout is treated as a stagnant round; the loop's bound prevents hanging.
}
int currentCount = driver.findElements(itemSelector).size();
long newHeight = ((Number) js.executeScript(
"return document.documentElement.scrollHeight")).longValue();
if (currentCount > previousCount || newHeight > oldHeight) {
stagnantRounds = 0;
} else {
stagnantRounds++;
}
previousCount = currentCount;
// Stop after two rounds with neither new items nor a taller document.
if (stagnantRounds >= 2) {
break;
}
}
// TakeScreenshot captures the current viewport. Scroll to the top first if that is desired.
// js.executeScript("window.scrollTo(0, 0)");
byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
Files.write(output, png);
System.out.println("Saved " + output.toAbsolutePath()
+ " after loading " + previousCount + " feed items.");
} finally {
driver.quit();
}
}
}
What to change for your page
- Item selector: choose a selector that matches each repeated entry, not a shared wrapper. Verify it returns the expected count.
- End condition: prefer a reliable site signal, such as an end-of-results element, a disabled “Load more” button, a known item count, or an application state exposed in the DOM. The example’s two stagnant rounds are only a fallback heuristic.
- Target and bounds: set
targetItemsandmaxRoundsto cap runtime and page size. Infinite feeds have no universal “finished” state. - Wait timeout: adjust per-round and initial waits for the site’s network and rendering behavior. A fixed sleep is simpler but usually wastes time or wakes before content is ready.
- Screenshot position: the example captures the final scroll position. Scroll to the top before the screenshot if you need the first viewport; capture at each position if you need a sequence of viewport images.
3. Choose the right capture output
Viewport screenshot
TakesScreenshot.getScreenshotAs(OutputType.FILE) or BYTES captures the current browser context. In this example that means the visible viewport at the current scroll offset. Save it directly, or copy the returned file to a chosen destination.
Full-page image
There is no site-independent guarantee that a WebDriver screenshot will include all content beyond the viewport. For a long dynamically loaded feed, first load the desired entries. Then use a browser-specific full-page capture technique if needed, and validate it against your browser and Selenium versions. Chrome DevTools Protocol provides Page.captureScreenshot; Selenium’s Java DevTools API has exposed a captureBeyondViewport option in version-specific APIs. This option affects capture bounds; it does not load the feed for you. See the [Chrome DevTools Protocol Page domain](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot) and the [Selenium DevTools v124 Java API](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/devtools/v124/page/Page.html).
A very tall full-page image may use substantial memory, exceed image dimension limits, or be difficult to inspect. For long feeds, a sequence of viewport captures or a PDF may be more manageable than one enormous bitmap.
Capture each loaded viewport
When the goal is to preserve all loaded content visually, capture a screenshot after each scroll step and name files by sequence number. This avoids relying on a browser’s full-page stitching behavior. It creates overlapping images, so keep the scroll increment consistent and record each scroll position if you need to reconstruct the feed later.
4. Reliable loading strategies and edge cases
- Wait for content, not just scrolling: scrolling only changes position. Wait for an item count increase, a loading spinner to disappear, a response-driven state to appear, or another page-specific signal.
- Nested scroll containers: some feeds scroll inside a panel. Find that element and scroll it with JavaScript or actions; scrolling
windowwill not trigger the panel’s loader. - Virtualized lists: frameworks may remove off-screen nodes and reuse a small number of DOM elements. Item count can stay constant even as content changes. Track a stable item ID/text, scroll position, or application signal instead.
- Lazy-loaded images: an entry may appear before its image is downloaded or decoded. Wait for visible images to have
complete == trueand a nonzeronaturalWidth, or wait for a page-specific ready state before capture. - Sticky headers and overlays: these are part of the rendered viewport. Close overlays when permitted, or hide them in a controlled test environment with page-specific CSS.
- Duplicate or changing content: live feeds can insert new entries above the current position. Track unique IDs and use a maximum scroll count to avoid looping indefinitely.
- Authentication and consent: establish the required session and handle consent before measuring or capturing. Do not assume a fresh browser profile has the same content as an authenticated user.
- Site access controls: follow the site’s terms and access rules. Do not attempt to bypass CAPTCHAs or other access controls.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot contains only the first screen | The script captured before scrolling, or only one viewport was requested. | Scroll and wait for the desired content first. Capture each viewport or use a browser-specific full-page method after loading. |
| The loop stops immediately | The item selector matches no elements, the end marker is always visible, or the target count is already met. | Inspect the live DOM, verify selectors and counts, and check the end-marker condition. |
| The loop times out on every round | The page loads through a nested scroller, uses a different content signal, or is blocked from loading. | Scroll the actual container and wait for a stable page-specific signal. Check the browser console and network behavior. |
| The count never increases on a virtualized feed | Off-screen elements are recycled instead of appended. | Track item IDs or text at each position, or use a site-specific completion signal rather than DOM count. |
| Images are missing or blank | Images are lazy-loaded or still decoding when capture occurs. | Scroll images into view and wait for image readiness or a site-specific render condition. |
SessionNotCreatedException |
Chrome and driver versions or runtime environment are incompatible, or Chrome cannot start. | Use a supported browser setup, let Selenium Manager resolve the driver where available, and inspect browser startup logs and headless arguments. |
ElementClickInterceptedException or no scroll effect |
An overlay intercepts interaction, or the page scrolls a different element. | Identify the scroll container, wait for overlays to settle, and scroll the correct element. |
| Screenshot is clipped or unexpectedly sized | Viewport dimensions differ between runs, device scale affects pixels, or the output is only viewport-sized. | Set a consistent window size before navigation and distinguish viewport capture from full-page capture. |
6. Performance, reliability, and cost
Each scroll-and-wait round adds latency. Use an explicit condition that matches the page, choose a realistic target count, and set a maximum round count and timeout. Avoid polling the whole DOM or capturing a screenshot after every tiny scroll unless the task requires it. Large feeds consume browser memory, especially when images and scripts remain loaded.
For repeatable results, pin the browser and Selenium versions in your build environment, set a fixed viewport, use a stable account and page state, and log the final item count, scroll rounds, and capture path. A network-idle condition can be unreliable on pages with persistent requests; a specific item or loading-state condition is often better. Selenium itself has no per-screenshot API charge in this example, but browser compute, CI minutes, storage, and maintenance have costs.
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request captures a URL as an image or PDF; see the [ScreenshotNeo documentation](https://screenshotneo.com/docs/) for request options. Its capture flow accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. AI agents can use its MCP server tools, including take_screenshot, get_page_info, and capture_pdf.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status} ${await res.text()}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
With Node.js, save the response body using your runtime’s file API; for Node’s built-in modules, replace the final line with await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))) in an async context.
One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. This one-call capture is useful for a rendered URL, but it does not replace Selenium when you need to drive a logged-in browser session, scroll a particular feed to a custom condition, or capture a sequence of scroll positions. [Visit ScreenshotNeo](https://screenshotneo.com) and [sign up for 1,000 free screenshots a month with no card](https://screenshotneo.com/account/sign-up/).
8. FAQ
Can Selenium know when every infinite-scroll page is finished?
No universal signal exists. Use the site’s end marker, known result count, disabled load-more control, or a bounded no-growth rule.
Will one screenshot contain the whole feed?
Not with the standard viewport capture. Load the content first, then use a validated browser-specific full-page capture or save multiple viewport screenshots.
Should I use fixed sleeps?
Only as a last resort for a page with no observable signal. Explicit waits tied to content or loading state are usually more reliable and efficient.


