Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Scroll the specific container, not the page. In Puppeteer, select the intended div, change its scrollTop (or use its locator’s scroll method), and verify that the same element moved. Use a hovered page.mouse.wheel() when the application must receive a real wheel event, or scrollIntoView() when you need a particular child revealed.

Why multiple scrollbars cause Puppeteer scripts to scroll the wrong place

A page can have a document scrollbar, a sidebar scrollbar, a table body scrollbar and nested panels at the same time. A command aimed at the page may move the document while the content you need remains hidden. Wheel input can also be consumed by whichever nested element is under the pointer.

Treat scrolling as a target-selection problem:

  • Known container and exact distance: set that element’s scrollTop.
  • User-like wheel behavior: move the pointer over the intended region and call page.mouse.wheel().
  • Known descendant: call scrollIntoView() on the child you need to expose.

Always read the selected element’s position before and after the operation. scrollTop is the vertical content offset; an element without scrollable overflow stays at zero, and a value beyond its range is limited to the maximum available offset (MDN: scrollTop).

Complete Puppeteer example: scroll a specific div

The following CommonJS script opens a page, waits for #results, checks that it has vertical overflow, advances that container by 300 pixels, and reports the result. Replace the URL and selector with the page you automate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();

  await page.goto('https://example.com/app', {waitUntil: 'networkidle2'});
  const container = await page.waitForSelector('#results');

  const before = await container.evaluate(el => ({
    scrollTop: el.scrollTop,
    scrollHeight: el.scrollHeight,
    clientHeight: el.clientHeight,
    overflowY: getComputedStyle(el).overflowY
  }));

  if (before.scrollHeight <= before.clientHeight) {
    throw new Error('The selected element has no vertical scrollable overflow');
  }

  await container.evaluate(el => { el.scrollTop += 300; });

  const after = await container.evaluate(el => ({
    scrollTop: el.scrollTop,
    maxScrollTop: el.scrollHeight - el.clientHeight
  }));

  console.log({before, after});
  await browser.close();
})();

For an exact position instead of an increment, use el.scrollTop = 500. The browser clamps the value to the container’s maximum. Puppeteer’s page-interaction guide also documents element scrolling through a locator, for example await page.locator('#results').scroll({scrollTop: 300}) (Puppeteer page interactions). The direct DOM assignment is useful when you need to inspect dimensions and the resulting position in one evaluation.

Choose the right scrolling method

Method Best for How it targets the right region Main caveat
scrollTop or locator scroll() Deterministic offsets and repeatable tests Operates directly on the selected element No movement occurs when the element has no overflow
page.mouse.wheel() Pages whose handlers, lazy loading or virtual lists depend on wheel events Move the pointer over the intended container first A nested element or event handler may consume the wheel
scrollIntoView() Making a known row, button or other descendant visible The browser scrolls ancestor containers as needed It aligns the target rather than advancing by a chosen pixel amount

Use direct positioning for a known container, wheel input when event-driven behavior matters, and scrollIntoView when the test’s real assertion is that a particular descendant is visible.

Scroll with a wheel event over the intended div

Wheel input is coordinate-based. Obtain the element’s bounding box, move the mouse to its center, then send the delta:

const box = await page.$('#results');
if (!box) throw new Error('Could not find #results');

const rect = await box.boundingBox();
if (!rect) throw new Error('#results is not visible');

const before = await box.evaluate(el => el.scrollTop);
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2);
await page.mouse.wheel({deltaY: 300});
await page.waitForFunction(
  (selector, oldTop) => document.querySelector(selector)?.scrollTop !== oldTop,
  {}, '#results', before
);
const after = await box.evaluate(el => el.scrollTop);
console.log({before, after});

Puppeteer defines Mouse.wheel() as dispatching a mouse-wheel event and shows moving the pointer over an element before sending the delta (Mouse.wheel() API). If the value does not change, inspect the element under the pointer: a nested scroll area may have consumed the event. You may also need to dispatch several smaller deltas for an application that loads content incrementally.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reveal a known descendant with scrollIntoView

When the goal is “show row 120” rather than “move 500 pixels,” target the descendant:

const target = await page.waitForSelector('#target-row');
await target.evaluate(el => {
  el.scrollIntoView({block: 'nearest', inline: 'nearest'});
});
await target.isIntersectingViewport();

Puppeteer’s ElementHandle.scrollIntoView() provides the same purpose through the automation protocol or the element’s own method (ElementHandle.scrollIntoView()). The DOM API supports block positions such as start, center, end and nearest. Its container option can choose all ancestors or only the nearest scrollable ancestor where supported (MDN: scrollIntoView()).

nearest usually causes the least disruptive movement in nested layouts. Choose center when a screenshot or visual assertion needs consistent framing.

Identify the correct scrollable element

Make the selector specific

Prefer an ID, a data attribute, or a relationship to a stable heading over a generic class shared by several panels:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const panels = await page.$$('.scroll-panel');
console.log('matches:', panels.length);
const results = await page.waitForSelector('[data-testid="results-panel"]');

If several matches are legitimate, select by a known child, surrounding label, or index only when the order is stable. A selector that accidentally picks the document wrapper can make every later diagnosis misleading.

Check overflow and computed styles

const metrics = await page.$eval('#results', el => {
  const style = getComputedStyle(el);
  return {
    scrollTop: el.scrollTop,
    scrollHeight: el.scrollHeight,
    clientHeight: el.clientHeight,
    overflowY: style.overflowY,
    canScroll: el.scrollHeight > el.clientHeight &&
      ['auto', 'scroll', 'overlay'].includes(style.overflowY)
  };
});
console.log(metrics);

scrollHeight > clientHeight indicates content taller than the visible box. The computed overflow value helps distinguish a genuinely scrollable panel from an element whose content merely extends outside the layout.

Wait for content before measuring

Virtualized lists and lazy-loaded rows may not have their final height immediately after navigation. Wait for a stable child or application-specific loading marker before reading dimensions:

await page.waitForSelector('#results [data-row-id]');
await page.waitForFunction(() => !document.querySelector('.loading-spinner'));

Do not use an arbitrary delay when a selector or state change is available; a delay can be too short on a slow run and unnecessarily long on a fast one.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verification patterns for reliable tests

Assert movement and bounds

const result = await page.$eval('#results', el => {
  const before = el.scrollTop;
  el.scrollTop += 300;
  const after = el.scrollTop;
  return {
    before,
    after,
    max: Math.max(0, el.scrollHeight - el.clientHeight),
    moved: after !== before
  };
});
if (!result.moved && result.max > 0) {
  throw new Error('Scroll command did not move the selected container');
}

At the bottom, after === max is expected. A zero change is only a success when the element has no overflow or is already at its limit.

Verify the intended container, not just a visible result

A page screenshot can look different even when the wrong panel moved. Record the selected element’s scrollTop, and, for nested layouts, record likely competing containers while diagnosing:

const positions = await page.$$eval('.scroll-panel', els =>
  els.map((el, i) => ({i, scrollTop: el.scrollTop}))
);
console.log(positions);

Account for smooth scrolling

If CSS uses scroll-behavior: smooth, a wheel or scrollIntoView call may animate. Wait for the target position or temporarily disable smooth behavior in a test-only style rather than asserting immediately after dispatch.

Troubleshooting multiple-scrollbar failures

“Element not found” or a timeout

  • Confirm navigation reached the expected URL and frame.
  • Wait for the panel’s stable selector instead of querying immediately.
  • If the panel is inside an iframe, obtain the matching frame and query there.
  • Check that the selector is not generated per session.

scrollTop stays at zero

  • Compare scrollHeight and clientHeight; there may be no overflow.
  • Inspect overflow-y; hidden or visible may prevent scrolling.
  • You may have selected a wrapper while a child owns the scrollbar. Inspect ancestors and descendants for the element whose dimensions change.
  • The panel may be at its top or bottom limit; try a smaller positive or negative offset and read the maximum.

The wheel scrolls the page instead

  • Move the pointer inside the actual scrollable box, not merely its heading.
  • Check for a nested element under the pointer and compare all candidate scrollTop values.
  • Ensure an overlay, modal or sticky layer is not intercepting the pointer.
  • Use direct scrollTop when you do not need wheel-event behavior.

The target row is still not visible

  • Wait for the row to be rendered if the list is virtualized.
  • Call scrollIntoView({block: 'center'}) on the row itself.
  • Check for a sticky header covering the aligned position; center or a small follow-up offset may be preferable.
  • Verify visibility with Puppeteer’s viewport check or a bounding-box intersection, not only selector existence.

Content loads only after scrolling

Use wheel input if the site listens specifically for wheel events, then wait for the new row or network state. If the site responds to programmatic scrolling, increment scrollTop in a loop and stop when the target selector appears or the position reaches its maximum.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, determinism and cost considerations

Direct element scrolling normally requires one browser evaluation and is deterministic, making it suitable for repeatable visual or functional tests. Wheel input involves pointer movement and event dispatch, so it better reproduces interaction but can vary with nested handlers, momentum and lazy loading. scrollIntoView minimizes the number of operations when a target is known, although the resulting alignment depends on layout and sticky elements.

For long lists, avoid repeatedly taking full-page screenshots while searching. Scroll in measured increments, wait for the specific content you need, and stop as soon as it is present. Keep selectors and assertions tied to stable application semantics rather than pixel coordinates.

Or skip the browser setup

If you only need an image or PDF of a URL rather than browser-level control, ScreenshotNeo provides a GET endpoint. See the ScreenshotNeo documentation for parameters and response headers.

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}`);

Before capture, ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each 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. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Can I scroll horizontally inside the same div?

Yes. Read and set scrollLeft, or pass a horizontal value to the locator’s scroll method. Apply the same before-and-after verification used for scrollTop.

Does changing scrollTop trigger the site’s scroll listener?

It changes the element’s position, but application behavior can differ from a user wheel event. If lazy loading or analytics depends on wheel or scroll events, test the page and use page.mouse.wheel() when necessary.

Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

Which Puppeteer version should I use?

Use the version installed by your project and check the current Puppeteer API pages for that release. Method signatures and locator capabilities can evolve, while the DOM properties and methods described by MDN remain browser APIs.

Frequently Asked Questions

Can I scroll horizontally inside the same div?

Yes. Read and set scrollLeft, or pass a horizontal value to the locator’s scroll method. Apply the same before-and-after verification used for scrollTop.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does changing scrollTop trigger the site’s scroll listener?

It changes the element’s position, but application behavior can differ from a user wheel event. If lazy loading or analytics depends on wheel or scroll events, test the page and use page.mouse.wheel() when necessary.

Which Puppeteer version should I use?

Use the version installed by your project and check the current Puppeteer API pages for that release. Method signatures and locator capabilities can evolve, while the DOM properties and methods described by MDN remain browser APIs.

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.