HasData
Back to all posts

How to Scroll Page using Selenium in Python

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:

MethodItems loadedTriggers / 10 stepsTime
window.scrollTo(scrollHeight)+909/1026 s
scrollIntoView on last element+909/1028 s
Keys.END+909/1027 s
window.scrollBy(0, 800)+404/1043 s
ActionChains scroll_by_amount(800)+404/1044 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 up

In 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 scrolling

Unlike 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 end

Avoid 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 document

Everything 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

MethodTriggers lazy loadingWorks inside overflowWorks inside iframeNotes
scrollTo(scrollHeight)YesNoYes (after switch)Best for main-page infinite scroll
scrollIntoViewYesNoYesTargets a specific element
Keys.ENDYesNoRarelyRequires body focus
scrollBy(fixed px)SometimesNoYesMisses threshold ~half the time
ActionChains scroll_by_amountSometimesNoYesSame as scrollBy
scrollTop loopYesYesYesOnly 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.

Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting