ScreenshotNeo

BlogHow-to

How to Use Puppeteer Locators to Find and Interact With Page Elements

Use Puppeteer’s Locator API to find elements and click, fill, hover, or scroll them with automatic readiness checks and retries.

By the ScreenshotNeo team4 October 20269 min read

Puppeteer’s Locator API is the recommended way to select and interact with page elements. Create a locator with page.locator(selector), then call an action such as click(), fill(), hover(), or scroll(). Locator actions wait for the target to be present and ready, and retry when readiness conditions are not met.

For example, click a button with await page.locator('button').click(), or fill a field with await page.locator('input[name="email"]').fill('dev@example.com'). This guide covers selector choices, common actions, readiness behavior, navigation, lower-level alternatives, troubleshooting, and a complete runnable script.

1. Install Puppeteer and run a locator script

Use the Puppeteer version installed in your project; the API can change between releases. The documentation covered here identifies version 25.12.0. Install Puppeteer in a Node.js project with:

npm install puppeteer

Save this as locator-example.mjs. It launches Chromium, loads a page, finds an input, fills it, and clicks a button. Replace the example URL and selectors with elements that exist on your target page.

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

  // Replace these selectors with elements on the page you automate.
  const search = page.locator('input[name="q"]');
  await search.fill('Puppeteer locators');
  await page.locator('button[type="submit"]').click();

  console.log('Submitted the search form');
} finally {
  await browser.close();
}

The example assumes that the page actually has those form controls. If it does not, Puppeteer will keep waiting according to the locator’s configured timeout and then report an error. See the troubleshooting section for how to diagnose that and other common failures.

2. Choose a selector that identifies the intended element

Call page.locator() for a page, or frame.locator() when the target lives inside a frame. A basic CSS selector works as-is:

const submit = page.locator('button[type="submit"]');
await submit.click();

Puppeteer also supports its own selector syntax for text, accessibility role and name, XPath, and queries that cross shadow roots. Use a selector that describes the element you mean and is not tied to incidental layout details. For example, a button’s accessible name or a form control’s stable name is often clearer than a long chain of nested CSS classes.

// CSS selector
await page.locator('input[name="email"]').fill('dev@example.com');

// Text selector (Puppeteer-specific syntax)
await page.locator('::-p-text(Continue)').click();

// Accessibility role and name (Puppeteer-specific syntax)
await page.locator('::-p-aria([role="button"])[name="Continue"]').click();

// XPath selector (Puppeteer-specific syntax)
await page.locator('::-p-xpath(//button[@type="submit"])').click();

Selector syntax is part of Puppeteer’s API, not necessarily valid in browser-native document.querySelector(). Consult the current Puppeteer selector documentation for exact syntax and supported combinations for your installed version.

3. Click, fill, hover, and scroll with locators

The Locator API has actions for common user interactions. Actions operate on the located value after Puppeteer’s readiness checks.

Click an element

await page.locator('button[type="submit"]').click();

By default, clicking checks that the element is present, in the viewport, visible, enabled, and has a stable bounding box across two consecutive animation frames. If it is not ready, the locator operation retries.

Fill a form control

await page.locator('input[name="email"]').fill('dev@example.com');
await page.locator('textarea[name="message"]').fill('Please send the release notes.');
await page.locator('select[name="region"]').fill('eu');

fill() determines an appropriate way to set the value at runtime. Documented supported targets include contenteditable, select, textarea, and input. For checkboxes, radio buttons, and switches, pass a boolean:

await page.locator('input[name="updates"]').fill(true);
await page.locator('input[name="updates"]').fill(false);

Use the value expected by the page’s control. For example, a select’s option value may differ from its visible label.

Hover and scroll

await page.locator('[data-menu="products"]').hover();
await page.locator('footer').scroll();

Hover can reveal menus or tooltips. Scroll moves the located element into view as needed for interaction. A hover-dependent target may still need a separate locator after the menu opens.

Wait for a condition or refine a locator

Locators also provide filter(), map(), wait(), and waitHandle(). A filter predicate can express a condition that must be met; locator operations retry while the expectation does not match. A mapper transforms the located value. The exact callback types depend on the installed API version, so check its type definitions and current reference when using these methods.

// Wait until this locator is actionable/ready before continuing.
const continueButton = page.locator('button.continue');
await continueButton.wait();
await continueButton.click();

4. Understand locator readiness, retries, and options

A locator stores a strategy for finding a value and performing an action, rather than just returning a one-time element handle. This lets Puppeteer re-evaluate the target when the page changes and retry an operation if readiness conditions are not satisfied.

Behavior or option What it is for
Presence Wait for the target to exist before acting.
Visibility Require the target to be visible; the locator API also offers visibility configuration.
Viewport handling Control whether the target must be in or brought into the viewport.
Enabled state Wait for a control to be enabled when the action requires it.
Stable bounding box Wait for geometry to remain stable across consecutive animation frames, which helps avoid clicking a moving target.
Timeout Configure how long the locator waits before giving up.
Cloning/configuration Build a configured locator while retaining the underlying selection strategy.
race(locators) Act on competing locator choices while ensuring only one locator receives the action.

Prefer the default readiness checks first. If an action times out, identify which condition the page violates before changing visibility, viewport, enabled-state, or stability settings. Disabling a check can make a flaky interaction appear to work while hiding the reason the target was not ready.

5. Handle navigation caused by a click

When a click triggers navigation, start waiting for navigation at the same time as the click. Starting the wait separately can race with a fast navigation and miss the event.

const [response] = await Promise.all([
  page.waitForNavigation(),
  page.locator('a.next').click(),
]);

console.log('Navigated to:', response?.url() ?? page.url());

waitForNavigation() can resolve with no response for some navigation types, so the optional chaining in the example handles that case. If the interaction updates the page without a navigation, wait for a locator or another page condition instead.

6. Use frames and specialized lower-level APIs when needed

For content inside an iframe, locate the frame and create the locator there. The frame must first be available on the page:

const frame = page.frames().find(candidate => candidate.url().includes('/embedded-form'));
if (!frame) {
  throw new Error('Embedded form frame was not found');
}

await frame.locator('input[name="email"]').fill('dev@example.com');
await frame.locator('button[type="submit"]').click();

Use page.waitForSelector() or an ElementHandle when the locator API does not provide the specialized functionality you need. waitForSelector() waits for DOM availability and returns a handle, but a later action on that handle does not automatically gain locator retry behavior. Dispose handles when finished to avoid retaining them unnecessarily.

const handle = await page.waitForSelector('canvas.chart');
if (!handle) {
  throw new Error('Chart canvas was not found');
}

try {
  // Use the handle only for an operation that needs this concrete element.
  const tagName = await handle.evaluate(element => element.tagName);
  console.log(tagName);
} finally {
  await handle.dispose();
}

Some page methods such as page.click(selector), page.type(selector), and page.hover(selector) are implemented using waitForSelector() for backward compatibility. Prefer locators for ordinary interactions when working with the current API.

7. Troubleshoot common locator failures

Symptom Likely cause Fix
Locator times out because no element appears The selector is wrong, the page has not reached the state you expect, or the target is inside a frame or shadow root. Inspect the rendered page and selector; wait for the correct page state; use a frame locator or supported shadow-root selector syntax where appropriate.
Click keeps retrying or times out The element is hidden, outside the viewport, disabled, or moving. Check the page state and target geometry. Scroll or wait for the UI transition to finish; only adjust readiness options after identifying the cause.
fill() fails for a control The target is not a supported fill target, the selector matches the wrong element, or the value is unsuitable for the control. Target an input, textarea, select, or contenteditable element; use a boolean for checkbox, radio, or switch controls; use the option value expected by a select.
Navigation wait never resolves as expected The click updates content without navigating, or the navigation wait was started after the click. Put waitForNavigation() and the click in the same Promise.all() when navigation is expected. Otherwise, wait for the page condition that changes.
Element is found in one run but not another The page renders asynchronously or the selector depends on unstable markup. Choose a more meaningful selector and use locator readiness or a page-state wait rather than a fixed delay where possible.
Memory grows during a long run using handles Returned ElementHandle objects remain retained. Dispose each handle in a finally block, or use a locator when a concrete handle is unnecessary.

8. Performance, reliability, and cost

Locator readiness checks and retries help coordinate interactions with pages that render asynchronously, but they still take time when a target is delayed or never becomes actionable. A specific, resilient selector and a condition that matches the actual UI state help avoid unnecessary waits. Use timeouts deliberately: a very short timeout can fail during normal rendering, while a long timeout can make a real selector bug slow to diagnose.

Keep browser lifetime and cleanup explicit in automation scripts. Close pages or the browser when work is complete, and dispose of handles returned by lower-level APIs. For a workload that only needs a rendered screenshot rather than browser interaction, running your own Puppeteer browser adds browser setup and maintenance that a screenshot API can avoid. ScreenshotNeo charges only for clean shots: bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with the outcome indicated by X-Page-Verdict and X-Billed response headers.

9. Or skip the browser setup

If you need a screenshot rather than an interactive browser session, ScreenshotNeo takes a URL in one API request and returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Read the ScreenshotNeo API documentation for request options. The following calls use https://stripe.com as the target; replace it with the page you are authorized to capture and provide your API key.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js

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())));

ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device and viewport settings, retina scale, PDF layout controls, HTML/CSS rendering, custom CSS and JavaScript, clicking before capture, hiding selectors, wait conditions, request blocking, custom headers and cookies, user agent and authorization, timezone and geolocation, transparent backgrounds, resizing, configurable cache TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI spec. Parameter names used by other screenshot APIs also work to ease migration.

1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing gives two months free. Visit ScreenshotNeo for the product details, then sign up for 1,000 free screenshots a month with no card.

10. FAQ

Are Puppeteer locators the same as browser CSS selectors?

No. Plain CSS selectors work, but Puppeteer also offers its own selector syntax for text, accessibility attributes, XPath, and queries across shadow roots.

Should I replace every ElementHandle with a locator?

Use locators for normal selection and interaction. Keep handles for specialized work that needs a concrete element reference, and dispose of each handle when finished.

Can a locator be used inside an iframe?

Yes. Create it from the relevant Frame with frame.locator() after identifying that frame.

Does a click always cause navigation?

No. Many interactions update the current page in place. Wait for navigation only when the action is expected to navigate; otherwise, wait for the resulting page condition.