Selenium gives you five distinct ways to scroll a page, and they are not interchangeable. I ran all five against a real infinite-scroll page (quotes.toscrape.com/scroll, 10 scroll steps each) and the results split into two groups:
| Method | Items loaded | Triggers / 10 steps | Time |
|---|---|---|---|
window.scrollTo(scrollHeight) | +90 | 9/10 | 26 s |
scrollIntoView on last element | +90 | 9/10 | 28 s |
Keys.END | +90 | 9/10 | 27 s |
window.scrollBy(0, 800) | +40 | 4/10 | 43 s |
ActionChains scroll_by_amount(800) | +40 | 4/10 | 44 s |
The fixed-pixel methods (scrollBy and ActionChains) loaded half as many items. An 800 px step does not reliably reach the loading threshold, and the page fires a new batch only when the viewport edge is close enough to the document bottom.
Overflow containers and popups with lazy loading need a scrollTop loop, which is covered in Scrolling an Element with Overflow.
All examples use this template. Replace the # scrolling code goes here comment with whichever method you want to test.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
url = "https://demo.opencart.com/"
chrome_options = Options()
chrome_options.add_argument("--headless=new")
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
# scrolling code goes here
driver.quit()Using JavaScript Executor
JavaScript execution bypasses focus and visibility requirements that affect Action Chains and keyboard events.
Scroll to a specific element
scrollIntoView takes any element Selenium can locate, no matter where it sits on the page.
element = driver.find_element(By.ID, "carousel-banner-1")
driver.execute_script("arguments[0].scrollIntoView(true);", element)scrollIntoView works on any element regardless of its position. To center the element in the viewport instead of aligning it to the top, pass a block option:
driver.execute_script(
"arguments[0].scrollIntoView({ behavior: 'smooth', block: 'center' });",
element
)The block: 'center' option keeps tall elements from scrolling past the relevant part.
Scroll to a specific position
Bottom, top, and a fixed offset each use the same scrollTo call with different y values.
# scroll to the bottom
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# scroll to the top
driver.execute_script("window.scrollTo(0, 0);")
# scroll to an exact pixel offset
driver.execute_script("window.scrollTo(0, 1500);")scrollTo takes absolute coordinates. It does not depend on where the viewport currently is.
Scroll by coordinates
Positive values scroll right and down, negative values scroll left and up.
# scroll down 500 px from current position
driver.execute_script("window.scrollBy(0, 500);")
# scroll up 500 px
driver.execute_script("window.scrollBy(0, -500);")
# scroll right
driver.execute_script("window.scrollBy(500, 0);")scrollBy offsets from the current scroll position. This works for fixed steps, but as the benchmark shows, a fixed pixel value misses the loading threshold roughly half the time on infinite-scroll pages. Prefer scrollTo(scrollHeight) when your goal is to trigger lazy loading.
Using Action Class
Use Action Chains when you need to combine scrolling with clicks, hover states, or drag operations. The gesture emulation lets you chain them into one .perform() call.
actions = ActionChains(driver)
# scroll to a specific element
element = driver.find_element(By.ID, "carousel-banner-1")
actions.move_to_element(element).perform()
# chain scroll with a click
actions.move_to_element(element).click().perform()
# scroll by a fixed amount (Selenium 4.2+)
actions.scroll_by_amount(0, 800).perform()scroll_by_amount and scrollBy share the same fixed-step limitation. A fixed pixel value misses the loading threshold roughly half the time, giving 4 triggers out of 10 in the benchmark instead of 9. move_to_element on the last visible item adjusts to actual content height and clears the threshold every time.
Keyboard keys inside Action Chains
Add PAGE_DOWN and PAGE_UP when you need keyboard scroll as part of a larger action chain.
actions.send_keys(Keys.PAGE_DOWN).perform()
actions.send_keys(Keys.PAGE_UP).perform()Keyboard keys inside Action Chains are useful when you need to mix keyboard input with other chained actions in a single sequence.
Scrolling with Keyboard Events
Keyboard scrolling sends keys directly to the body element:
body = driver.find_element(By.TAG_NAME, "body")
body.send_keys(Keys.END) # jump to bottom
body.send_keys(Keys.HOME) # jump to top
body.send_keys(Keys.PAGE_DOWN) # scroll one viewport down
body.send_keys(Keys.PAGE_UP) # scroll one viewport upIn the benchmark, Keys.END matched scrollTo(scrollHeight) exactly, with 9/10 triggers and 90 items loaded. It requires no JavaScript, and on infinite-scroll pages where you always want to land at the bottom, it needs no setup beyond finding the body element.
The main limitation is focus. If the page has a focused input field or modal, body.send_keys may type into that element instead of scrolling. execute_script("window.scrollTo(...)") does not have this problem.
Scrolling an Element with Overflow
When a container has overflow: auto or overflow: scroll (popup follower lists, sidebar feeds, chat windows), window.scrollTo and keyboard events only move the main page. You need to target the element directly.
Jump to the bottom of an overflow container
Setting scrollTop to scrollHeight jumps immediately to the current bottom of the container.
scrollable = driver.find_element(By.ID, "popup-list")
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight;", scrollable)This loads one batch. For a popup with lazy loading, the content below does not exist yet when you jump to the current scrollHeight, so nothing new loads.
Loop with WebDriverWait for lazy-loaded popups
For overflow elements that load content as you scroll:
from selenium.webdriver.support.ui import WebDriverWait
scrollable = driver.find_element(By.ID, "popup-list")
item_selector = (By.CSS_SELECTOR, "#popup-list .item")
for _ in range(20):
before = len(driver.find_elements(*item_selector))
driver.execute_script(
"arguments[0].scrollTop += arguments[0].offsetHeight;",
scrollable
)
try:
WebDriverWait(driver, 3).until(
lambda d: len(d.find_elements(*item_selector)) > before
)
except Exception:
break # no new content loaded, stop scrollingUnlike a single scrollHeight jump, offsetHeight per step scrolls exactly one visible viewport of the container. New items render at the bottom, scrollHeight grows, and the next iteration scrolls into the newly loaded area. WebDriverWait advances as soon as new items appear instead of waiting a fixed interval.
Tips and Tricks
Modals, infinite-scroll feeds, and iframes each interfere with the methods above in a different way.
Infinite scrolling on the main page
For an infinite-scroll page (social feeds, product catalogues, search results):
from selenium.webdriver.support.ui import WebDriverWait
import time
item_selector = (By.CSS_SELECTOR, "div.product")
for _ in range(10):
before = len(driver.find_elements(*item_selector))
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
try:
WebDriverWait(driver, 4).until(
lambda d: len(d.find_elements(*item_selector)) > before
)
except Exception:
break # reached the endAvoid time.sleep(2) as the wait mechanism. A fixed pause adds 20 seconds to a 10-step loop even when new content loads in 0.3 seconds. WebDriverWait exits as soon as the condition is met.
Pop-ups and overlays
Before scrolling, dismiss any modal that intercepts pointer events:
try:
close_button = driver.find_element(By.ID, "modal-close")
close_button.click()
except Exception:
pass # no modal present
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")The try/except keeps the script running if the modal was already closed or never appeared.
Frames and nested elements
To scroll content inside an iframe, switch context first:
iframe = driver.find_element(By.ID, "iframe_id")
driver.switch_to.frame(iframe)
nested = driver.find_element(By.ID, "nested_element_id")
driver.execute_script("arguments[0].scrollIntoView(true);", nested)
driver.switch_to.default_content() # return to main documentEverything inside the frame behaves as a separate document. scrollIntoView, scrollTop loops, and scrollTo all work the same way once you have switched context.
When to use which method
| Method | Triggers lazy loading | Works inside overflow | Works inside iframe | Notes |
|---|---|---|---|---|
scrollTo(scrollHeight) | Yes | No | Yes (after switch) | Best for main-page infinite scroll |
scrollIntoView | Yes | No | Yes | Targets a specific element |
Keys.END | Yes | No | Rarely | Requires body focus |
scrollBy(fixed px) | Sometimes | No | Yes | Misses threshold ~half the time |
ActionChains scroll_by_amount | Sometimes | No | Yes | Same as scrollBy |
scrollTop loop | Yes | Yes | Yes | Only option for overflow popups |
For main-page infinite scroll, scrollTo(scrollHeight) is the default. For overflow containers with lazy loading, the scrollTop += offsetHeight loop is the only method that reliably works. The benchmark on quotes.toscrape.com/scroll confirmed this (10 steps, 10 quotes per batch): both fixed-pixel methods stalled at 40 items while the jump-to-bottom methods reached 100.


