skip to content

In Selenium, how do Actions.scrollToElement() and Actions.scrollByAmount() differ, and which device do they drive?

level: juniorimportance: nice to knowfreq 31%

answer

  1. A third virtual device beside pointer and keyboard
  2. One names a target, the other a distance
  3. Deltas start at the viewport's top left
  4. The driver computes the distance for you
  5. scrollFromOrigin reaches a nested scroll panel

basics

~20 s

Both drive Selenium's virtual wheel device, added in Selenium 4.2. scrollToElement takes an element and scrolls it into view, letting the driver work out the distance; scrollByAmount takes pixel deltas from the viewport's top left corner.

solid answer

~40 s

Selenium 4.2 added a wheel input source alongside the pointer and keyboard, and `Actions` exposes it through `scrollToElement`, `scrollByAmount` and `scrollFromOrigin`. `scrollToElement(element)` names a target: if the element is outside the viewport the driver scrolls its bottom to the bottom of the viewport, and does nothing if it is already visible. `scrollByAmount(deltaX, deltaY)` names a distance instead, scrolling by those pixels with the origin at the viewport's top left - positive `deltaY` goes down. `scrollFromOrigin(WheelInput.ScrollOrigin, deltaX, deltaY)` combines both, letting you scroll a given distance from an element's centre, which is how you reach a nested scrollable panel. All three ride in the same single actions request, and none of them moves the pointer.

code

java · 14 lines
java
WebElement row = driver.findElement(By.cssSelector("[data-report-id='EXP-4471']"));
Actions actions = new Actions(driver);

actions.scrollToElement(row).perform();

actions.scrollByAmount(0, 400).perform();

WebElement listPanel = driver.findElement(By.id("approval-list"));
WheelInput.ScrollOrigin origin = WheelInput.ScrollOrigin.fromElement(listPanel);
actions.scrollFromOrigin(origin, 0, 400).perform();

WheelInput.ScrollOrigin offsetOrigin =
        WheelInput.ScrollOrigin.fromElement(listPanel, 0, -50);
actions.scrollFromOrigin(offsetOrigin, 0, 200).perform();

go deeper

for a junior

Know that Selenium has wheel scroll actions and which one takes an element versus pixel deltas. Reaching for scrollToElement when you just need a row on screen is the everyday answer.

for a middle

Explain that wheel is a third input source with its own sequence, that a scroll's duration counts toward the tick duration, and that scrolling never moves the pointer.

for a senior

Show when scrolling is redundant because the pointer methods already work against an in-view centre point, and why fixed pixel deltas are the part that breaks across window sizes.

for a principal

Own whether hand-driven scrolling belongs in the suite at all, since it encodes viewport assumptions that multiply across every browser and screen size the team supports.

## A third virtual device Selenium 4.2 added a **wheel input source** alongside the pointer and the keyboard, and `Actions` exposes it through three methods: `scrollToElement`, `scrollByAmount` and `scrollFromOrigin`. They build `WheelInput` scroll interactions, which the browser dispatches as real wheel events, and they travel in the same single actions request as any other chain step. The distinction between the first two is about **what you name**: a target element, or a distance. Three things follow from the wheel being a device of its own: - It has its own sequence, so a scroll and a pointer move can occupy the same tick and happen together. - All three methods return the builder, so scrolls chain freely with clicks and key presses in one sequence. - Nothing in your code is browser-specific; the driver turns the scroll interaction into the browser's own wheel events. ## scrollToElement - name the element `scrollToElement(WebElement)` scrolls the given element into the viewport if it is not already there, bringing the bottom of the element to the bottom of the viewport. You do not supply a distance; the driver works out what is needed. ```java WebElement row = driver.findElement(By.cssSelector("[data-report-id='EXP-4471']")); new Actions(driver).scrollToElement(row).perform(); ``` On a long expense-report approval list this is what you want when the goal is "make row EXP-4471 reachable" and you do not care how far down it is. If the element is already visible, the scroll is a no-op rather than a jump. ## scrollByAmount - name the distance `scrollByAmount(int deltaX, int deltaY)` scrolls by the given pixel deltas with the origin at the **top left corner of the viewport**. A positive `deltaY` scrolls down, a negative one scrolls up; a negative `deltaX` scrolls left. ```java new Actions(driver).scrollByAmount(0, 400).perform(); ``` Nothing here refers to an element, so this is the method for "move the page a screenful" rather than "reach this row". It is also the one people misuse: it scrolls the viewport, so if the approval list is itself a scrollable panel inside the page, `scrollByAmount` moves the page around the panel and leaves the list where it was. ## Comparing them | | `scrollToElement` | `scrollByAmount` | |---|---|---| | Argument | A `WebElement` | Pixel deltas `deltaX`, `deltaY` | | Origin | The element | Top left of the viewport | | Distance | Computed by the driver | Exactly what you pass | | Already-satisfied case | Does nothing if in view | Always scrolls by the deltas | | Typical use | Reach a specific report row | Advance the page by a known amount | ## scrollFromOrigin - name both The third method covers the case neither of the others does: scroll a **given distance** from a **given origin**. The origin is a `WheelInput.ScrollOrigin`, built either from an element or from the viewport, optionally with an offset: - `WheelInput.ScrollOrigin.fromElement(panel)` - the centre of `panel` - `WheelInput.ScrollOrigin.fromElement(panel, 0, -50)` - 50 pixels above that centre - `WheelInput.ScrollOrigin.fromViewport()` and `fromViewport(x, y)` - the viewport's top left, plus offsets ```java WheelInput.ScrollOrigin origin = WheelInput.ScrollOrigin.fromElement(listPanel); new Actions(driver).scrollFromOrigin(origin, 0, 400).perform(); ``` This is the one that scrolls a nested scrollable region: put the origin inside the approval list's own scroll container and the wheel events land there rather than on the document. If the element used as an origin is itself out of view, the driver first scrolls its bottom to the bottom of the viewport, exactly as `scrollToElement` would, and then applies the deltas. ## Timing and pointer position All three take their duration from the `Actions` constructor, which defaults to 250 ms and can be set with `new Actions(driver, Duration.ofMillis(50))`. Like a `pointerMove`, a scroll's duration counts towards the tick duration, so a chain of scrolls is paced rather than instantaneous. Scrolling does **not** move the virtual pointer. If a chain scrolls a row into view and then uses a no-argument `contextClick()`, the click lands wherever the pointer already was - which, after a scroll, is very often not over the row. Target the element explicitly, or `moveToElement(row)` first. ## When wheel scrolling is not the answer - The pointer convenience methods already scroll for you: `moveToElement`, `clickAndHold` and friends work against an element's in-view centre point, so an explicit scroll before them is usually redundant. - A wheel event is not a keyboard scroll. A list that only responds to Page Down needs keys, not `scrollByAmount`. - Deltas are pixels, not rows or pages. Hard-coded numbers tuned on one viewport size do not survive a different window, whereas `scrollToElement` does.

  • The approval list is a scrollable panel inside the page and scrollByAmount does nothing to it. Why?
    `scrollByAmount` puts the scroll origin at the viewport's top left, so the wheel events land on the document and move the page around the panel. Build a `WheelInput.ScrollOrigin.fromElement(panel)` and use `scrollFromOrigin` instead, so the events originate inside the panel's own scroll container.
  • After scrolling a row into view, a no-argument contextClick() opens the wrong menu. What happened?
    Scrolling drives the wheel device and does not move the pointer, so the pointer is still wherever the last pointer action left it - and the page moved underneath it. Target the element explicitly with `contextClick(row)`, or `moveToElement(row)` before the click.

saying these in an interview costs you the question

  • Thinks scrollByAmount takes an element rather than pixel deltas
  • Expects scrollByAmount to scroll a nested panel rather than the page
  • Assumes a wheel scroll also moves the virtual pointer
  • Believes scrollToElement re-scrolls an element that is already visible
  • Hard-codes pixel deltas tuned for one window size