DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
browser automation

How to Scroll to and Click Buttons with Puppeteer (Locator-First Guide)

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.

In current Puppeteer, the safest way to scroll to an off-screen button and click it is usually to let a locator do both jobs: await page.locator('button#save').click();. Puppeteer brings the element into the viewport, waits for it to be visible, enabled and geometrically stable, then clicks it. Use an explicit scroll() call only when you need to control or demonstrate the scrolling step, and use DOM scrolling for unusual alignment or nested containers.

The recommended scroll-and-click pattern

Locators are Puppeteer’s recommended way to select and interact with elements. A locator click performs readiness checks before acting:

  • The target is resolved.
  • It is brought into the viewport when necessary.
  • It is visible and enabled.
  • Its bounding box is stable across animation frames.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'networkidle0'});

await page.locator('button#save').click();

await browser.close();

This is preferable to manually scrolling first because the click operation still verifies that the button is ready after any layout movement.

When to scroll explicitly

Use a locator scroll

If your test needs a visible, separate scroll step, keep the same locator and call scroll() before clicking:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const save = page.locator('button#save');
await save.scroll();
await save.click();

Locator scrolling checks viewport presence, visibility and bounding-box stability. The locator API also lets you control whether viewport enforcement is used with setEnsureElementIsInTheViewport().

Use DOM scrolling for alignment control

Manual DOM scrolling is useful when a sticky header covers the button, a nested scroll area needs special handling, or you require a particular alignment. Scroll the element, then return to the locator for the final readiness checks:

await page.$eval('button#save', el =>
  el.scrollIntoView({block: 'center', inline: 'nearest'})
);
await page.locator('button#save').click();

Do not treat scrollIntoView() as a replacement for readiness checks. A scroll can trigger lazy rendering, animations or an overlay; the locator click gives Puppeteer a chance to wait for those conditions.

Selectors that keep clicks reliable

CSS selectors work by default, but a stable, user-meaningful selector is more important than the selector syntax. Prefer a unique accessible role/name, test ID or durable attribute over generated class names.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.locator('button[data-testid="save"]').click();
await page.locator('aria/Save').click();
await page.locator('text/Save').click();

Use the form that best identifies the control a user would recognize. Confirm uniqueness when a page can render duplicate “Save” buttons, such as a desktop and mobile toolbar. With the lower-level API, a selector matching several elements clicks the first match:

await page.click('button#save');

page.click() fetches the element, scrolls it into view if needed, and clicks its center. It rejects when no element matches, so an unexpectedly broad or unstable selector can fail or target the wrong control.

Clicks that trigger navigation

Start the navigation wait and the click together. Creating the navigation wait afterward can miss a fast navigation and produce a race:

const [response] = await Promise.all([
  page.waitForNavigation({waitUntil: 'networkidle0'}),
  page.locator('button#save').click(),
]);

console.log('Loaded:', response?.url());

Choose the wait condition that matches your page. networkidle0 waits for no active network connections, but pages with analytics, polling or streaming requests may never become idle. In those cases, wait for a specific destination or post-navigation element instead.

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

Single-page applications: wait for the result, not navigation

Many buttons update the current document without navigating. After clicking, wait for an observable state change:

await page.locator('button#save').click();
await page.locator('[role="status"]').wait();

A more precise assertion can target a success message, changed text, a newly enabled control or a URL change handled by the application router. Avoid adding an arbitrary delay when a concrete locator can express the condition.

Buttons inside iframes

The page locator cannot cross into an iframe’s document. Find the frame first, then create the locator in that frame:

const frame = page.frames().find(f => f.url().includes('/checkout'));
if (!frame) throw new Error('checkout frame not found');

await frame.locator('button#save').click();

The frame-scoped locator retains the same viewport, visibility, enabled-state and stability behavior. For a cross-origin frame, use its frame object rather than attempting to query the child document from the parent page.

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

Nested scrolling containers and sticky headers

A button may be off-screen inside a panel even though the main page is already at the top. First try the locator’s normal click or scroll(); Puppeteer can scroll the relevant element into view. If a panel has a custom offset or a sticky toolbar hides the center point, align it yourself:

const button = 'section.results button[data-testid="next"]';
await page.$eval(button, el =>
  el.scrollIntoView({block: 'center', inline: 'nearest'})
);
await page.locator(button).click();

If the panel itself requires wheel movement, scroll the container rather than the document, then let the locator perform the final click. This avoids coupling the test to a particular page height.

Why a click still fails after scrolling

Symptom Likely cause Fix
No element matches Selector is wrong, rendered late or scoped to the wrong frame. Verify the selector, wait for the target, and use a frame-scoped locator when applicable.
Click intercepted Cookie banner, modal, sticky header or chat widget covers the center. Dismiss or hide the overlay, scroll the button to a clear position, then retry the locator click.
Element is not clickable The control is disabled or its geometry is moving. Wait for the enabled state and for animations or layout changes to settle.
Wrong button clicked Selector matches multiple controls. Use a unique role/name, test ID or stable attribute and verify the rendered variants.
Navigation timeout The page keeps background connections open. Use a more suitable navigation condition or wait for a destination element instead of network idle.
Works visually but not in automation The button is inside an iframe or shadow-root composition. Use the appropriate frame or locator selector strategy and inspect the rendered structure.

A practical diagnostic sequence

  1. Confirm the page reached the expected URL and that the button is rendered.
  2. Check that the selector identifies exactly one intended control.
  3. Try page.locator(selector).click() before adding custom scrolling.
  4. If the scroll itself matters, call locator.scroll(), then click the same locator.
  5. For alignment problems, use scrollIntoView({block: 'center'}) and retain the locator click.
  6. Inspect overlays, disabled state, animations and frame boundaries.
  7. If the click changes pages, pair it with waitForNavigation() in Promise.all; for an SPA, wait for the resulting UI state.

Performance, reliability and test design

Locator-first code generally avoids unnecessary scrolling commands and fixed delays. Keep selectors independent of pixel coordinates and document height so tests survive responsive layouts. Explicit scrolling adds value when you are testing the scroll behavior itself, validating lazy loading, or working around a known alignment constraint; otherwise it is extra state to maintain.

Use a fresh page or a controlled test state when overlays and persisted consent settings can change the result. When a target appears only after an interaction, wait for that interaction’s concrete output rather than sleeping for a guessed number of milliseconds. For bulk test suites, stable selectors and state-based waits reduce retries more effectively than longer global timeouts.

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

Or skip the browser setup

If your goal is a clean image of a page rather than an interactive test, ScreenshotNeo provides a single screenshot request. It accepts consent banners before capture 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 the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

See the ScreenshotNeo API documentation for the full option set. A one-call WebP capture looks like this:

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

ScreenshotNeo supports full-page and element captures, device presets and custom viewports, dark mode, retina scale, PDF settings, custom CSS and JavaScript, clicks before capture, wait conditions, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture and a usage API. Every feature is included on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to begin.

Quick reference

Approach Automatic readiness checks Scroll control Best use
locator.click() Viewport, visibility, enabled state and stable geometry Automatic Most button interactions
locator.scroll() then click Yes, on both operations Explicit step Tests that need to show or control scrolling
scrollIntoView() then locator click Final click still checks readiness Precise alignment Sticky headers and unusual containers
page.click() Scrolls and clicks center, but offers less locator-oriented composition Automatic Legacy or simple lower-level code

Frequently Asked Questions

Should I use a fixed delay before clicking an off-screen button?

Usually no. Prefer a locator’s readiness checks or wait for a specific post-action element; fixed sleeps are sensitive to machine speed and network conditions.

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

Can Puppeteer click a button that is technically outside the viewport?

Yes. Both locator clicks and the lower-level page click bring a matching element into view before clicking, provided it is rendered and otherwise actionable.

What if the button opens a new tab instead of navigating the current page?

Listen for the browser’s target or page-creation event and then apply a locator in the new page; a current-page navigation wait will not represent a newly opened tab.

The Bottom Line

Start with page.locator(selector).click(). Add scroll() for an explicit step, DOM scrolling for alignment edge cases, and frame-scoped locators for iframe content. Synchronize navigation or SPA state with the result you actually expect.

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.