How to Scroll with Puppeteer Mouse Wheel Events
Use page.mouse.wheel() to send vertical or horizontal wheel input in Puppeteer. Learn when to use locator.scroll(), target nested containers, and debug scroll behavior.
Use await page.mouse.wheel({deltaY: 500}) to send vertical wheel input in Puppeteer; use deltaX for horizontal input. If a particular element should receive the wheel event, move the pointer over that element first. For a selected element that should scroll by specified offsets, Puppeteer also provides page.locator(selector).scroll({scrollLeft, scrollTop}).
This guide covers both APIs, runnable setup, nested scroll areas, assertions, common failures, and the limits of synthetic mouse input. Puppeteer documents that wheel input dispatches a mousewheel event, but does not promise a fixed relationship between a delta and the resulting scroll position. Puppeteer Mouse.wheel() reference.
1. Install Puppeteer and send a wheel event
In a new project, install Puppeteer and save this as scroll.mjs. The package includes a compatible browser download during installation.
npm install puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
// Send vertical wheel input from the mouse's current position.
await page.mouse.wheel({deltaY: 500});
// Send horizontal wheel input when the page has a horizontal scroll area.
await page.mouse.wheel({deltaX: 200});
} finally {
await browser.close();
}
Run it with node scroll.mjs. The call returns a promise, so await it before checking page state or closing the browser. Wheel options accept optional numeric deltaX and deltaY values. A positive or negative delta indicates direction of input, but do not treat its magnitude as an exact number of pixels the document will move. See the MouseWheelOptions reference.
2. Scroll a specific element
Wheel input is delivered at the pointer location. For a nested panel, find its bounding box, move the pointer inside it, and then send the wheel event. This follows the pattern in Puppeteer’s official method example.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
await page.goto('https://example.com/dashboard', {waitUntil: 'domcontentloaded'});
const panel = await page.locator('.scrollable-panel').waitHandle();
const box = await panel.boundingBox();
if (!box) {
throw new Error('Scroll panel has no visible bounding box');
}
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.wheel({deltaY: 400});
await panel.dispose();
} finally {
await browser.close();
}
The mouse coordinates used by Puppeteer are main-frame CSS pixels measured from the viewport’s top-left corner. The box-center approach is convenient, but check that the center is not covered by another element and that the target actually has overflow in the requested direction. Puppeteer’s Mouse reference describes the coordinate system.
Use locator.scroll() when the desired offsets matter
If you need to scroll a known element by requested horizontal and vertical offsets, use the locator API:
await page.locator('.scrollable-panel').scroll({
scrollLeft: 0,
scrollTop: 400,
});
Puppeteer documents that locator scrolling uses mouse wheel events. The locator also waits for the element to be in the viewport, visible, and stable across two animation frames before scrolling. These preconditions can make it a better fit when the goal is simply to scroll a selected element. For a test specifically about pointer targeting or wheel-event handling, use page.mouse.wheel(). See the page interactions guide.
| Need | Use | Reason |
|---|---|---|
| Exercise wheel handling or hover-dependent behavior | page.mouse.move() and page.mouse.wheel() |
You control the pointer location and send wheel input. |
| Scroll a selected element by horizontal and vertical offsets | page.locator(selector).scroll() |
It targets a locator and applies its documented readiness checks. |
| Know whether the page actually moved | Inspect the relevant element’s scroll position after either action | Input delta alone does not establish the resulting position. |
3. Verify scrolling and handle dynamic pages
For assertions, measure the element’s position before and after the interaction. This keeps the test focused on observed page state instead of assuming a delta maps to a particular distance.
const panel = page.locator('.scrollable-panel');
const before = await panel.evaluate(element => element.scrollTop);
await panel.scroll({scrollLeft: 0, scrollTop: 400});
const after = await panel.evaluate(element => element.scrollTop);
if (after <= before) {
throw new Error(`Expected panel to move down; before=${before}, after=${after}`);
}
For a wheel-specific test, take the same measurements around the pointer and wheel calls. If the page lazily loads content, wait for an application-specific signal after scrolling, such as a newly rendered item or a loading indicator disappearing. Avoid a fixed sleep unless the page offers no observable completion condition: a delay can be too short on a slow run and waste time on a fast one.
For infinite scrolling, one event may not load the next batch. Repeat a bounded interaction and wait for the expected content or for the document height to change. Include a maximum iteration count so a broken end condition cannot leave automation running indefinitely. For horizontal content, target the horizontal container and use deltaX, or use locator offsets with a positive scrollLeft.
4. Troubleshooting
| Symptom | Likely cause | What to check or change |
|---|---|---|
| The page does not move | The pointer is outside the scrollable region, the region has no overflow in that direction, or the page handles the event without default scrolling. | Move the pointer into the intended area; inspect its dimensions and scrollable content; check the page’s wheel handlers and resulting scroll position. |
| The wrong container scrolls | The pointer is over the document or a different nested region. | Use the target’s bounding box and move into its visible area before calling wheel(). For a known element, try locator scroll(). |
| The distance differs from the delta | Wheel delta is input, not a guaranteed final scrollTop change. Page handlers and nested containers affect the result. |
Read scrollTop or scrollLeft before and after; assert a condition appropriate to the page rather than a universal pixel mapping. |
| The element handle has no box | The element may be hidden, detached, or not laid out. | Wait for the element to appear and become visible; reacquire a detached handle; check the box before calculating coordinates. |
| Locator scrolling times out | The locator’s visibility, viewport, or stable-bounding-box preconditions were not met before its timeout. | Check that the selector identifies the intended element and that it can become visible and stable. Set an appropriate locator timeout or use the lower-level mouse API when pointer placement is required. |
| Automation differs from a physical mouse | Puppeteer’s mouse events are synthetic and do not fully reproduce physical-device interaction. | Use an actual device/browser setup for checks that require hardware fidelity; treat Puppeteer results as synthetic input behavior. |
5. Reliability, performance, and version notes
Await each interaction and verify a page condition when the next step depends on its result. A wheel call completing means the input action was dispatched; it does not establish that an animation, lazy load, or application update has finished. Prefer condition-based waits for those outcomes. Keep loops bounded and close the browser in a finally block so a failed assertion does not leave a browser process open.
For performance, avoid launching a fresh browser for every scroll in a suite: reuse a browser where appropriate, while giving each independent test a page and predictable initial state. The API references do not publish timing or throughput guarantees, so actual runtime depends on browser startup, site behavior, and waits.
The cited current Puppeteer method and interaction documentation identifies version 25.12.0; the MouseWheelOptions page retrieved for this guide identifies version 25.3.0. APIs can vary by installed package version. Check the typings and documentation corresponding to your installed Puppeteer version if a signature or behavior differs.
6. Or skip the browser setup
If the outcome you need is a screenshot rather than a test of wheel-event behavior, ScreenshotNeo returns a screenshot or PDF from one API request. It supports full-page capture, element capture, custom viewport options, and other capture settings. See the ScreenshotNeo API documentation.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports page verdict and billing headers. ScreenshotNeo also has an MCP server for AI agents, with screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
7. FAQ
Does a wheel event scroll the page automatically?
It dispatches wheel input. Whether and where content moves depends on pointer location, scrollable areas, and page event handling, so inspect the resulting position.
Can Puppeteer scroll sideways?
Yes. Supply deltaX to page.mouse.wheel(), or set scrollLeft using locator scroll().
Should I use JavaScript scrollIntoView instead?
Use it when you need to bring an element into view and do not need to exercise wheel input. Use the mouse or locator wheel-based APIs when wheel behavior is the subject of the interaction.
Does Puppeteer guarantee the same result as a real mouse?
No. Puppeteer documents its mouse input as synthetic and says it does not fully reproduce what a physical mouse can do.


