How to Scroll Pages with Selenium
Learn Selenium scrolling with wheel actions and JavaScript, including elements, nested containers, lazy loading, errors, browser support, and complete code.
Selenium gives you two dependable ways to scroll: use the Actions API for wheel-like input, or execute JavaScript in the current page to use DOM scrolling methods. Use wheel actions when you need realistic scrolling, a fixed distance, or an element-based origin. Use JavaScript when you need direct control over an element or a scroll container.
The examples below use Python and Selenium 4. The wheel actions described in Selenium’s official guide are labeled Chromium Only, so verify support for your browser and binding before depending on them in cross-browser tests.
Choose the right scrolling method
| Goal | Recommended method | Why |
|---|---|---|
| Bring a known element into view | scroll_to_element or JavaScript scrollIntoView() |
Targets the element directly. |
| Move a fixed number of pixels | scroll_by_amount(0, pixels) |
Expresses a precise vertical or horizontal delta. |
| Scroll from an element or viewport point | scroll_from_origin |
Useful for nested regions and offset-based movement. |
| Scroll a nested container | JavaScript on the container | Lets you set scrollTop or call scrollIntoView on descendants. |
| Support browsers where wheel actions are unavailable | JavaScript | Uses the WebDriver script execution API in the current window or frame. |
Set up a complete Python example
Install Selenium, start a browser driver, and run this script. Selenium Manager can obtain a compatible driver in current Selenium releases; in locked-down environments, provide the driver through your normal browser-management process.
python -m pip install -U selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
# options.add_argument("--headless=new") # Enable for CI if required.
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)
try:
driver.get("https://example.com")
# Wait for the document to be usable before scrolling.
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
target = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "h1")))
ActionChains(driver).scroll_to_element(target).perform()
# Give the browser a moment to paint after an action when an immediate
# screenshot or measurement follows.
wait.until(lambda d: d.execute_script("return window.scrollY >= 0"))
finally:
driver.quit()
Scroll an element into view
Wheel input with scroll_to_element
Selenium’s wheel guide describes this as the common target-element case. It positions the page so the element’s bottom is at the bottom of the viewport. Actions do not automatically scroll a target into view before another action, so explicitly scroll first when the element may be outside the viewport.
from selenium.webdriver.common.action_chains import ActionChains
button = driver.find_element(By.ID, "load-more")
ActionChains(driver).scroll_to_element(button).perform()
button.click()
JavaScript with scrollIntoView
execute_script runs synchronous JavaScript in the current window or frame. The browser’s scrollIntoView method is useful when you need alignment options or broader browser coverage.
element = driver.find_element(By.CSS_SELECTOR, "#results")
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});",
element,
)
For a sticky header, center alignment usually leaves the element visible instead of placing it underneath the header. You can also use block: 'start' or block: 'end'.
Scroll by a defined distance
scroll_by_amount(delta_x, delta_y) scrolls from the upper-left of the viewport. Positive vertical values move down; negative values move up. Positive horizontal values move right; negative values move left.
from selenium.webdriver.common.action_chains import ActionChains
# Down 500 CSS pixels.
ActionChains(driver).scroll_by_amount(0, 500).perform()
# Back up 250 CSS pixels.
ActionChains(driver).scroll_by_amount(0, -250).perform()
# Horizontal movement.
ActionChains(driver).scroll_by_amount(400, 0).perform()
The delta is a requested scroll amount, not a guarantee that the document will move exactly that far. A page can clamp movement at its top or bottom, intercept wheel input, or route the event to a nested scrollable element.
Scroll from an element or viewport origin
scroll_from_origin accepts a scroll origin and horizontal and vertical deltas. An origin can be an element, optionally with an offset, or a viewport coordinate. If an element origin is outside the viewport, Selenium first brings it into view. An offset that falls outside the viewport raises an exception.
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions.wheel_input import ScrollOrigin
panel = driver.find_element(By.CSS_SELECTOR, ".results-panel")
origin = ScrollOrigin.from_element(panel)
ActionChains(driver).scroll_from_origin(origin, 0, 300).perform()
To scroll relative to an offset inside an element, create the origin with an offset supported by your Python Selenium version:
origin = ScrollOrigin.from_element(panel, 0, 100)
ActionChains(driver).scroll_from_origin(origin, 0, 300).perform()
A viewport origin is useful when a page has a known coordinate-based scroll area:
origin = ScrollOrigin.from_viewport(200, 300)
ActionChains(driver).scroll_from_origin(origin, 0, 250).perform()
Scroll a nested container
Scrolling the window does not necessarily move a div with overflow: auto or overflow: scroll. Set the container’s scrollTop or scroll a child into view.
container = driver.find_element(By.CSS_SELECTOR, ".messages")
driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollHeight;",
container,
)
To move a particular item inside that container:
message = driver.find_element(By.CSS_SELECTOR, ".messages .message:last-child")
driver.execute_script(
"arguments[0].scrollIntoView({block: 'nearest'});",
message,
)
block: 'nearest' minimizes movement and is often preferable in a panel because it avoids jumping the outer page.
Scroll until content appears
Infinite-scroll pages require a loop that performs a scroll, waits for either new content or the end condition, and stops safely. Do not use an unbounded loop.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver, 10)
previous_height = driver.execute_script("return document.body.scrollHeight")
for _ in range(20):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
try:
wait.until(lambda d: d.execute_script("return document.body.scrollHeight") > previous_height)
except Exception:
# No additional height appeared during the wait.
break
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height <= previous_height:
break
previous_height = new_height
# Always leave the loop with a finite upper bound and a meaningful stop condition.
A stronger stop condition is a visible “no more results” marker or a known item count. Height alone can change because of images loading, layout shifts, or expanding content.
Handle lazy-loaded images and content
Some pages load images only when an element approaches the viewport. Scroll in increments and wait for the image’s complete property or a non-empty natural width.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
images = driver.find_elements(By.CSS_SELECTOR, "img[data-src], img[loading='lazy']")
for image in images:
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});",
image,
)
WebDriverWait(driver, 10).until(
lambda d, img=image: d.execute_script(
"return arguments[0].complete && arguments[0].naturalWidth > 0;",
img,
)
)
Wait correctly after scrolling
Scrolling itself may finish before application code renders the next section. Prefer an explicit wait for a DOM condition over a fixed sleep.
from selenium.webdriver.support import expected_conditions as EC
ActionChains(driver).scroll_to_element(target).perform()
wait.until(EC.visibility_of(target))
wait.until(lambda d: target.rect["y"] >= 0)
For a network-backed infinite list, wait for a new child, a spinner to disappear, or a page-specific end marker. A short delay is a fallback when no observable condition exists, but it is slower and less reliable than a state-based wait.
JavaScript scrolling patterns
Window coordinates
# Scroll to an absolute document position.
driver.execute_script("window.scrollTo(0, 1200);")
# Move relative to the current position.
driver.execute_script("window.scrollBy(0, 500);")
# Return to the top.
driver.execute_script("window.scrollTo(0, 0);")
Smooth scrolling
driver.execute_script(
"window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});"
)
Smooth scrolling is asynchronous. Wait for the desired element or position rather than assuming the movement has completed immediately.
Account for a fixed header
header_height = driver.execute_script(
"return document.querySelector('header')?.getBoundingClientRect().height || 0;"
)
driver.execute_script(
"window.scrollBy(0, -arguments[0]);",
header_height,
)
Node.js Selenium example
The JavaScript binding exposes WebDriver script execution and browser actions. Exact wheel-action method names can vary by binding version, so JavaScript scrolling is a portable baseline when you need a simple page-level operation.
import { Builder, By, until } from 'selenium-webdriver';
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get('https://example.com');
await driver.wait(until.elementLocated(By.css('h1')), 15000);
const heading = await driver.findElement(By.css('h1'));
await driver.executeScript(
"arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});",
heading
);
} finally {
await driver.quit();
}
Browser and binding compatibility
Selenium’s official wheel-actions page is labeled “Chromium Only.” Wheel input was introduced in Selenium 4.2, but availability of a particular method depends on the Selenium language binding and browser driver. Check the current official documentation for your target pair before using wheel actions in Firefox, Safari, or remote grids. JavaScript execution is the fallback documented by WebDriver and is often easier to keep consistent.
Common errors and fixes
| Error or symptom | Cause | Fix |
|---|---|---|
ElementNotInteractableException |
The element is outside the viewport, covered, hidden, or disabled. | Scroll it into view, wait for visibility, then check overlays and enabled state. |
MoveTargetOutOfBoundsException |
An origin offset falls outside the viewport. | Use a smaller offset, scroll the origin first, or use JavaScript. |
| Wheel action does nothing | The browser or driver does not support the wheel input used by the binding, or a nested region consumes the event. | Verify browser support, target the scrollable element, or set its scrollTop with JavaScript. |
| Element is still hidden under a sticky header | Default alignment placed it at the top edge. | Use block: 'center' or scroll back by the header height. |
| Infinite-scroll loop never ends | There is no bounded iteration or end condition. | Cap iterations and stop on unchanged height, an end marker, or a stable item count. |
| Stale element reference | The framework replaced DOM nodes after loading more content. | Locate the element again after the update instead of reusing the old reference. |
| Script runs in the wrong context | The target is inside an iframe. | Switch to the frame before locating or scrolling: driver.switch_to.frame(...). |
| Page scrolls but content is missing | Lazy loading or rendering has not completed. | Wait for the image, row, spinner, or network-driven DOM condition that proves readiness. |
Performance and reliability checklist
- Use one targeted scroll for a known element instead of repeatedly scrolling the entire document.
- Prefer explicit waits for visible state or new content; avoid large fixed sleeps.
- Use bounded loops for infinite scrolling.
- Scroll nested containers directly when they own the scrollbar.
- Re-find elements after virtualized lists or React-style rerenders.
- Record the browser, driver, Selenium binding, and headless mode when diagnosing differences.
- For screenshots after scrolling, wait for fonts, images, and application rendering that affect the final layout.
Or skip the browser setup
If your goal is a clean screenshot rather than browser interaction, ScreenshotNeo provides a single request that returns PNG, JPEG, WebP, or PDF. Its API can load lazy images for full-page captures, capture a CSS-selected element, wait for a selector, delay, or network idle, and apply custom JavaScript or CSS. See the ScreenshotNeo API documentation for all options.
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}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Which Selenium method should I learn first?
Start with scroll_to_element for a known target and scroll_by_amount for fixed movement. Keep JavaScript scrolling available for nested containers and browser combinations where wheel actions are not supported.
Does Selenium automatically scroll before clicking?
Actions do not automatically scroll a target into view. Scroll explicitly, then wait for visibility and clickability.
Can I scroll an iframe’s document?
Yes. Switch into the iframe first, locate the element in that frame, and perform the scroll there. Switch back to the default content when finished.
Why did a 500-pixel scroll move less?
The browser clamps movement at a boundary or routes the wheel event to a different scrollable region. Inspect the active container and its current scroll range.
When is JavaScript preferable to wheel input?
Use JavaScript when you need direct DOM control, nested-container scrolling, alignment options, or a fallback for browsers not covered by the wheel guide.


