Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
When a page has several scrollable regions, scroll the intended div directly instead of scrolling the document and hoping the browser picks the right one. In Puppeteer, use the element’s scrollTop (or locator scrolling) for a precise offset, move the mouse over that region and send a wheel event for user-like behavior, or call scrollIntoView() when a particular descendant must become visible. Always compare the selected element’s scrollTop before and after the operation.
Choose the scrolling outcome first
Multiple scrollbars are a target-selection problem. Decide whether you need to advance a known container by a measured amount, reproduce a wheel gesture, or reveal a known child element. The correct API depends on that outcome:
| Goal | Best approach | Why |
|---|---|---|
| Move a known container by an exact amount | scrollTop or locator scrolling |
Deterministic and independent of pointer position |
| Trigger page behavior attached to wheel events | Hover the container, then page.mouse.wheel() |
Dispatches an input event like a user gesture |
| Reveal a known row, card, or control | scrollIntoView() |
Scrolls the required ancestor containers to expose that element |
Method 1: set the div’s scrollTop
Use a selector that uniquely identifies the scrollable region. This example advances a results panel by 300 CSS pixels:
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →const container = await page.waitForSelector('#results');
await container.evaluate(el => {
el.scrollTop += 300;
});
To jump to a known vertical offset, assign rather than increment:
#1 Best Overall
await container.evaluate(el => {
el.scrollTop = 500;
});
The browser clamps values beyond the available scroll range to the maximum. If the element has no scrollable overflow, scrollTop remains zero. The property represents the number of pixels of content hidden above the element’s viewport; see the MDN scrollTop reference.
Use Puppeteer’s locator scrolling API
Recent Puppeteer page-interaction documentation also exposes element scrolling through a locator:
await page.locator('#results').scroll({scrollTop: 300});
This is useful when your code already uses locators. For a fixed, repeatable position, direct DOM assignment remains explicit because it states exactly which property is being changed. Check the API supported by the Puppeteer version installed in your project in the official page-interactions guide.
Method 2: send a wheel event over the intended region
Wheel input is appropriate when the application listens for wheel events, implements custom scrolling, or should be exercised more like a user session. Move the pointer into the target box before dispatching the delta:
const box = await page.$('#results');
if (!box) throw new Error('Results container was not found');
const rect = await box.boundingBox();
if (!rect) throw new Error('Results container is not visible');
await page.mouse.move(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await page.mouse.wheel({deltaY: 300});
Puppeteer documents Mouse.wheel() as dispatching a mouse-wheel event; its example likewise places the pointer over the relevant element first. Read the Mouse.wheel() API reference. A nested panel under the pointer may consume the event, so verify which element moved instead of assuming the outer div did.
Rank #2
Wheel versus direct position changes
- Direct scrolling: choose it for deterministic offsets, repeated test steps, and pages with several nested scroll areas.
- Wheel input: choose it when event handlers, momentum-like behavior, or application code must receive a wheel event. Pointer location and event propagation then matter.
Method 3: reveal a known descendant with scrollIntoView()
If your real requirement is “make this row visible,” do not guess a pixel distance. Scroll the descendant itself:
await page.$eval('#target-row', el => {
el.scrollIntoView({block: 'nearest'});
});
Puppeteer also provides ElementHandle.scrollIntoView():
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallconst row = await page.waitForSelector('#target-row');
await row.scrollIntoView();
The method scrolls ancestor containers as needed. The DOM API accepts block alignment such as start, center, end, and nearest; it also documents a container choice of all or nearest. See the MDN documentation and Puppeteer’s ElementHandle.scrollIntoView() reference. Use nearest when minimizing movement in nested panes matters; choose another alignment when a test requires a stable visual position.
Identify the correct scrollbar safely
Prefer stable selectors
A class shared by every panel is not enough. Select by an ID, data attribute, accessible relationship, or a selector scoped to a known section:
const panel = await page.waitForSelector(
'[data-testid="results-panel"]'
);
If several matches are expected, inspect them and select by a stable attribute or their relationship to a known heading or child. Avoid relying on generated class names that change between builds.
Confirm that the element can scroll
const metrics = await page.$eval('#results', el => ({
scrollTop: el.scrollTop,
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
overflowY: getComputedStyle(el).overflowY
}));
console.log(metrics);
A vertical scrollbar requires content height greater than the element’s client height. If scrollHeight equals clientHeight, there is no vertical overflow to move. An overflow-y value such as auto or scroll is a useful diagnostic, but the dimensions and the actual before/after position are decisive.
Verify that the intended container moved
Record the position before and after every operation. This catches the common case where the document, an outer panel, or a nested child consumed the action:
const panel = await page.waitForSelector('#results');
const before = await panel.evaluate(el => el.scrollTop);
await panel.evaluate(el => {
el.scrollTop += 300;
});
const after = await panel.evaluate(el => el.scrollTop);
console.log({before, after});
For wheel scrolling, use the same check around page.mouse.wheel(). A value that does not change can mean the panel is already at an edge, has no overflow, is not the element under the pointer, or is being controlled by custom event logic.
Reliable patterns for dynamic pages
Wait for content before measuring
Virtualized lists and lazy-loaded rows may not have their final dimensions immediately. Wait for a selector that proves the panel is rendered, then measure. If a specific row is required, wait for that row before calling scrollIntoView().
Scroll in bounded increments
For an infinite list, increment scrollTop and re-check a sentinel or item count. Stop when the target appears or when the position reaches the maximum; do not assume a single large assignment loads every page of data.
Rank #4
Account for sticky headers
scrollIntoView({block: 'start'}) can place a row underneath a sticky header. Prefer nearest, a centered alignment, or a follow-up visibility assertion that accounts for the header’s height.
Troubleshooting common failures
The selector matches the wrong panel
Symptom: a scrollbar moves, but it is not the one your test needs. Fix: narrow the selector with a stable ID, data attribute, or parent-child relationship; inspect all matches and log identifying attributes.
scrollTop stays at zero
Symptom: assignment appears to do nothing. Causes: no overflow, the element is not the scrolling box, or CSS places scrolling on a parent. Compare scrollHeight and clientHeight, inspect computed overflow, and test the nearest ancestor that visibly owns the scrollbar.
Wheel input scrolls the page instead
Symptom: the document moves while the panel remains fixed. Fix: obtain the panel’s bounding box, move the pointer to its center, dispatch the wheel event, and compare the panel and document scrollTop values. A nested child under the pointer may be the actual event target.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesThe target is still hidden after scrollIntoView()
Symptom: the element is technically in view but covered or clipped. Fix: try block: 'center' or 'nearest', account for sticky overlays, and assert its bounding rectangle against the visible panel rather than checking existence alone.
The position changes intermittently
Symptom: a test passes locally but fails on slower runs. Fix: wait for the panel and its content, avoid racing layout changes, use direct position changes where possible, and verify the resulting scrollTop before continuing.
Performance, repeatability, and boundaries
- Direct DOM scrolling avoids pointer movement and is usually the simplest choice for deterministic test setup.
- Wheel events exercise more of the page’s input path but add dependence on coordinates, nested targets, and event handlers.
- Every scroll position is bounded by available overflow; requesting a value beyond the end settles at the maximum.
- For nested regions, verify the exact element you selected rather than relying on the visual location of a scrollbar.
- Use a target descendant when the assertion concerns visibility, not a particular pixel offset.
Or skip the browser setup:
If the goal is a clean screenshot rather than testing interactive scrolling, ScreenshotNeo provides a website screenshot API and MCP server. Its capture flow accepts cookie and consent banners before removing more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. AI agents can use its MCP tools—take_screenshot, get_page_info, and capture_pdf.
One GET request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for options such as full-page capture, element selectors, device presets, custom CSS or JavaScript, waiting rules, PDF output, and signed links. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Best Value
- Used Book in Good Condition
Complete examples in Python and Node.js
Python: scroll and verify
import asyncio
from puppeteer import ...
Puppeteer is a Node.js library; Python users typically drive Chromium with another automation client. If your project uses Puppeteer itself, the JavaScript examples above are the directly applicable implementation. Do not translate the API name mechanically without checking the automation library installed in your Python project.
Node.js: direct scrolling with an assertion
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
const panel = await page.waitForSelector('#results');
const before = await panel.evaluate(el => el.scrollTop);
await panel.evaluate(el => { el.scrollTop += 300; });
const after = await panel.evaluate(el => el.scrollTop);
if (after === before) {
throw new Error('The selected panel did not scroll');
}
await browser.close();
})();
Replace the URL and selector with the page under test. The examples in this article are illustrative; confirm exact method availability against the Puppeteer version installed in your project.
Frequently Asked Questions
Can I scroll horizontally inside the same div?
Yes. Use the element’s scrollLeft property or a locator scroll with a scrollLeft value, then verify that property before and after the operation.
How do I know whether the browser scrolled an ancestor?
Read scrollTop on the intended element and its relevant ancestors before and after the action. The value that changes identifies the scrolling box that consumed it.
Which method should a visual regression test use?
Use direct position assignment when the expected screenshot depends on a fixed offset; use scrollIntoView() when the assertion is about a particular element being visible.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

