Scrolling
Scroll elements into view, scroll by an amount, use the W3C wheel actions, handle infinite scroll and sticky headers, and know when Selenium scrolls for you.
Selenium scrolls an element into view automatically before clicking or typing into it. That covers most cases, so the first rule of scrolling is: do not scroll unless a command failed because of it. The exceptions are lazy-loaded content, sticky headers that cover the target after the automatic scroll, infinite feeds, and screenshots of a specific region. This lesson covers each.
Option 1: scrollIntoView via JavaScript
The most common approach, and the one that works in every browser and Selenium version.
WebElement footer = driver.findElement(By.id("footer"));JavascriptExecutor js = (JavascriptExecutor) driver;
// Align to top of viewportjs.executeScript("arguments[0].scrollIntoView(true);", footer);
// Center it, which avoids sticky headers and footersjs.executeScript("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", footer);footer = driver.find_element(By.ID, "footer")
# Align to top of viewportdriver.execute_script("arguments[0].scrollIntoView(true);", footer)
# Center itdriver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", footer)const footer = await driver.findElement(By.id('footer'));
await driver.executeScript('arguments[0].scrollIntoView(true);', footer);await driver.executeScript("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", footer);var footer = driver.FindElement(By.Id("footer"));var js = (IJavaScriptExecutor)driver;
js.ExecuteScript("arguments[0].scrollIntoView(true);", footer);js.ExecuteScript("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", footer);block: 'center' is the setting to remember. Aligning to the top puts the element under any fixed header, which then intercepts the click.
Option 2: The Actions API Wheel (Selenium 4.2+)
The W3C Actions API includes a wheel input device. Selenium 4.2 added scrollToElement, scrollByAmount and scrollFromOrigin so scrolling is a real user action rather than script injection. It fires the same events a trackpad would, which matters for infinite-scroll implementations that listen to wheel rather than scroll.
import org.openqa.selenium.interactions.Actions;import org.openqa.selenium.interactions.WheelInput;
Actions actions = new Actions(driver);
// Scroll until the element is in viewWebElement target = driver.findElement(By.id("load-more"));actions.scrollToElement(target).perform();
// Scroll the page by a delta (positive y = down)actions.scrollByAmount(0, 500).perform();
// Scroll from an origin: here, from the element's centre, 200px further downWheelInput.ScrollOrigin origin = WheelInput.ScrollOrigin.fromElement(target);actions.scrollFromOrigin(origin, 0, 200).perform();
// Scroll inside a scrollable container: origin at the container, delta applies to itWebElement panel = driver.findElement(By.cssSelector(".sidebar-scroll"));actions.scrollFromOrigin(WheelInput.ScrollOrigin.fromElement(panel), 0, 300).perform();from selenium.webdriver import ActionChainsfrom selenium.webdriver.common.actions.wheel_input import ScrollOrigin
actions = ActionChains(driver)
# Scroll until the element is in viewtarget = driver.find_element(By.ID, "load-more")actions.scroll_to_element(target).perform()
# Scroll the page by a delta (positive y = down)actions.scroll_by_amount(0, 500).perform()
# Scroll from an originorigin = ScrollOrigin.from_element(target)actions.scroll_from_origin(origin, 0, 200).perform()
# Scroll inside a scrollable containerpanel = driver.find_element(By.CSS_SELECTOR, ".sidebar-scroll")actions.scroll_from_origin(ScrollOrigin.from_element(panel), 0, 300).perform()const { Origin } = require('selenium-webdriver/lib/input');
const target = await driver.findElement(By.id('load-more'));
// Scroll until the element is in viewawait driver.actions().scroll(0, 0, 0, 0, target).perform();
// Scroll the page by a delta from the viewport originawait driver.actions().scroll(0, 0, 0, 500, Origin.VIEWPORT).perform();
// Scroll from an element origin, 200px further downawait driver.actions().scroll(0, 0, 0, 200, target).perform();using OpenQA.Selenium.Interactions;
var actions = new Actions(driver);var target = driver.FindElement(By.Id("load-more"));
// Scroll until the element is in viewactions.ScrollToElement(target).Perform();
// Scroll the page by a deltaactions.ScrollByAmount(0, 500).Perform();
// Scroll from an originvar origin = new WheelInputDevice.ScrollOrigin { Element = target, XOffset = 0, YOffset = 0 };actions.ScrollFromOrigin(origin, 0, 200).Perform();Safari does not implement the wheel action; fall back to JavaScript there.
Scrolling the Window by Pixels or to the Bottom
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("window.scrollBy(0, 800);"); // relativejs.executeScript("window.scrollTo(0, document.body.scrollHeight);"); // to bottomjs.executeScript("window.scrollTo({top: 0, behavior: 'instant'});"); // to top
Long y = (Long) js.executeScript("return window.scrollY;"); // current offsetdriver.execute_script("window.scrollBy(0, 800);")driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")driver.execute_script("window.scrollTo({top: 0, behavior: 'instant'});")
y = driver.execute_script("return window.scrollY;")await driver.executeScript('window.scrollBy(0, 800);');await driver.executeScript('window.scrollTo(0, document.body.scrollHeight);');await driver.executeScript("window.scrollTo({top: 0, behavior: 'instant'});");
const y = await driver.executeScript('return window.scrollY;');var js = (IJavaScriptExecutor)driver;
js.ExecuteScript("window.scrollBy(0, 800);");js.ExecuteScript("window.scrollTo(0, document.body.scrollHeight);");js.ExecuteScript("window.scrollTo({top: 0, behavior: 'instant'});");
var y = (long)js.ExecuteScript("return window.scrollY;");Use behavior: 'instant' in tests. Smooth scrolling animates over several frames, and a screenshot or click taken mid-animation lands in the wrong place.
Infinite Scroll
Feeds that load more items as you approach the bottom need a loop: scroll, wait for new items, repeat until the count stops changing or you have what you need. Waiting on the item count is the reliable signal; waiting a fixed time is not.
By items = By.cssSelector("[data-testid='feed-item']");JavascriptExecutor js = (JavascriptExecutor) driver;WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
int previous = -1;for (int page = 0; page < 20; page++) { int count = driver.findElements(items).size(); if (count == previous) break; // feed stopped growing previous = count;
js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); final int seen = count; try { wait.until(d -> d.findElements(items).size() > seen); } catch (TimeoutException e) { break; // nothing more loaded }}System.out.println("Loaded " + driver.findElements(items).size() + " items");items = (By.CSS_SELECTOR, "[data-testid='feed-item']")wait = WebDriverWait(driver, 10)
previous = -1for _ in range(20): count = len(driver.find_elements(*items)) if count == previous: break # feed stopped growing previous = count
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") try: wait.until(lambda d: len(d.find_elements(*items)) > count) except TimeoutException: break # nothing more loaded
print("Loaded", len(driver.find_elements(*items)), "items")const items = By.css("[data-testid='feed-item']");
let previous = -1;for (let page = 0; page < 20; page++) {const count = (await driver.findElements(items)).length;if (count === previous) break;previous = count;
await driver.executeScript('window.scrollTo(0, document.body.scrollHeight);');try { await driver.wait(async () => (await driver.findElements(items)).length > count, 10000);} catch { break;}}var items = By.CssSelector("[data-testid='feed-item']");var js = (IJavaScriptExecutor)driver;var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
int previous = -1;for (int page = 0; page < 20; page++){ int count = driver.FindElements(items).Count; if (count == previous) break; previous = count;
js.ExecuteScript("window.scrollTo(0, document.body.scrollHeight);"); try { wait.Until(d => d.FindElements(items).Count > count); } catch (WebDriverTimeoutException) { break; }}Cap the loop. A feed that never ends will otherwise run until the test timeout.
Sticky Headers That Intercept Clicks
The symptom is ElementClickInterceptedException with a message naming the header element. Selenium scrolled the target to the top edge and the header covers it. Fixes, in order of preference:
- Scroll with
block: 'center'before clicking (above). - Use
Actions.scrollToElement, which respectsscroll-marginCSS if the app sets it. - As a last resort, hide the header with JavaScript in the test environment.
WebElement link = driver.findElement(By.linkText("Terms"));((JavascriptExecutor) driver).executeScript( "arguments[0].scrollIntoView({block: 'center'});", link);link.click();link = driver.find_element(By.LINK_TEXT, "Terms")driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", link)link.click()const link = await driver.findElement(By.linkText('Terms'));await driver.executeScript("arguments[0].scrollIntoView({block: 'center'});", link);await link.click();var link = driver.FindElement(By.LinkText("Terms"));((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].scrollIntoView({block: 'center'});", link);link.Click();Full-Page Screenshots
Selenium’s getScreenshotAs captures the viewport in most browsers (Firefox captures the full page). To capture a long page elsewhere, either resize the window to the document height before the screenshot, or use CDP’s Page.captureScreenshot with captureBeyondViewport on Chromium. The resize approach works everywhere:
JavascriptExecutor js = (JavascriptExecutor) driver;long height = (Long) js.executeScript("return document.documentElement.scrollHeight;");driver.manage().window().setSize(new Dimension(1366, (int) height));File shot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);height = driver.execute_script("return document.documentElement.scrollHeight;")driver.set_window_size(1366, height)driver.save_screenshot("full-page.png")const height = await driver.executeScript('return document.documentElement.scrollHeight;');await driver.manage().window().setRect({ width: 1366, height });fs.writeFileSync('full-page.png', await driver.takeScreenshot(), 'base64');var height = (long)((IJavaScriptExecutor)driver).ExecuteScript("return document.documentElement.scrollHeight;");driver.Manage().Window.Size = new System.Drawing.Size(1366, (int)height);((ITakesScreenshot)driver).GetScreenshot().SaveAsFile("full-page.png");Headless browsers accept very tall windows; headed ones are capped by the screen.
Summary
- Selenium scrolls automatically before interacting; scroll manually only for lazy content, sticky headers, infinite feeds or screenshots.
scrollIntoView({block: 'center'})avoids fixed headers; the Actions wheel API produces real user events.- Drive infinite scroll loops by element count with a cap, never by sleeping.
- Use instant scrolling in tests; smooth scrolling causes mid-animation clicks and screenshots.