ScreenshotNeo

BlogHow-to

How to Filter Puppeteer Locators to Find the Right Element

Use Puppeteer’s locator filter to narrow a useful candidate set to the element you intend to interact with, with working examples and fixes for common mistakes.

By the ScreenshotNeo team4 October 20267 min read

To filter Puppeteer locators, start with a selector that finds a useful set of candidates, apply .filter(predicate) to express what distinguishes the intended element, then perform the locator action:

await page
  .locator('button')
  .filter(button => button.textContent === 'My button')
  .click();

This is the basic answer to “How to Filter Puppeteer Locators to Find the Right Element.” The predicate runs in the browser context, and the filter is part of Puppeteer’s retrying locator behavior; it is not JavaScript’s in-memory Array.filter(). For current details, see Puppeteer’s Page interactions guide, Locator API, and Page.locator API.

1. Build a useful candidate set

A filter is clearest when the initial selector already narrows the page to the right kind of element. If the target is a button, use button; if it is a link in a particular navigation region, scope to that region first. The predicate then captures the additional condition that identifies the target.

await page
  .locator('button')
  .filter(button => button.textContent === 'Save changes')
  .click();

Use the condition that reflects the page and the behavior you need. Puppeteer’s documented example uses exact textContent equality, but that is a pattern, not a universal rule. Whitespace, nested elements, localization, or dynamic labels may mean a different condition or selector is more suitable.

2. Pass Node.js values safely

The predicate is evaluated in the page’s browser context. It does not close over ordinary Node.js variables. This callback can therefore fail if buttonName is only defined in Node:

const buttonName = 'Save changes';

// A Node closure is not available to the browser-side predicate.
await page
  .locator('button')
  .filter(button => button.textContent === buttonName)
  .click();

When a value comes from Node, Puppeteer’s guide demonstrates embedding a safely serialized value with JSON.stringify in the predicate string:

const buttonName = 'Save changes';
const predicate = `button => button.textContent === ${JSON.stringify(buttonName)}`;

await page
  .locator('button')
  .filter(predicate)
  .click();

Serialization matters: interpolating raw input into JavaScript source can break quoting or change the expression. Keep the predicate small and use JSON serialization for the value.

3. Choose between a predicate and selector syntax

Approach Use it when
CSS A stable tag, class, attribute, or DOM relationship identifies the candidates.
.filter(predicate) A useful candidate set exists, but a custom condition such as exact textContent distinguishes the target.
Text selector Visible text is a good representation of the target. Puppeteer’s text selectors choose minimal elements containing the requested text and can search open shadow roots.
ARIA selector The computed accessible role and name describe the target. Puppeteer computes these from the accessibility representation, including relationships such as labelledby.
XPath An XPath expression describes the desired DOM relationship directly. Puppeteer uses the browser’s native Document.evaluate.
Shadow-DOM combinator The target is inside an open shadow root. >>> searches descendants at any depth; >>>> searches the immediate shadow root. These combinators have documented depth and open-shadow-root limitations.

Prefer the most direct selector that expresses stable user-facing meaning, and add a predicate when it makes an extra condition clearer. No selector style is universally best: the page markup and the target determine which is clearest. Puppeteer also documents legacy forms such as text/My text, aria/My label, and xpath///h2. They remain supported, while the current guide recommends the documented selector syntax; legacy prefixes run one non-CSS selector at a time and cannot combine selectors.

4. Understand retries and action checks

.filter(predicate) refines a locator by adding an expectation. Puppeteer retries when that expectation does not match. Locator actions also retry while their readiness conditions are not met, rather than returning an array of elements for immediate JavaScript processing.

For a click(), the guide documents checks that include whether the element is in the viewport, visible, enabled, and has a stable bounding box across two animation frames. Do not assume every locator action has identical preconditions. A filter that never matches can keep waiting until the locator’s configured timeout behavior ends the operation.

5. Scope the locator to the right page or frame

Create the locator from the page that owns the target. Puppeteer supports page.locator() and frame.locator(); an element inside an iframe must be located from the appropriate frame context.

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

await frame
  .locator('button')
  .filter(button => button.textContent === 'Continue')
  .click();

This example assumes the checkout frame URL is identifiable that way. Adapt the frame selection to the page rather than assuming a fixed frame URL.

6. Complete runnable example

Install Puppeteer in a Node.js project, save the following as filter-locator.js, then run it with Node. The example navigates to a page you control that contains a button labeled Save changes.

const puppeteer = require('puppeteer');

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

    const buttonName = 'Save changes';
    const predicate = `button => button.textContent === ${JSON.stringify(buttonName)}`;

    await page
      .locator('button')
      .filter(predicate)
      .click();
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Replace the example URL with your application URL and make sure the button exists there. The locator API is documented at Puppeteer Page interactions.

7. Troubleshooting

Symptom Likely cause Fix
The predicate throws a reference error for a Node variable. The callback runs in the browser context and cannot access a Node closure. Serialize the value into a predicate string with JSON.stringify, or use selector syntax that avoids passing a value.
The locator keeps waiting and then times out. No candidate satisfies the filter yet, the candidate selector is wrong, the text differs, or the page/frame context is incorrect. Check the selector and condition against the rendered page, confirm the intended frame, and ensure the target reaches the expected state.
The click is rejected because the element is not ready. The target may be outside the viewport, hidden, disabled, or moving; click has readiness checks. Wait for the application state to settle, verify visibility and enabled state, and confirm the element is in the viewport.
A text selector or predicate matches an unexpected element. Several elements may contain the text, or nested content and whitespace may affect textContent. Start with a more specific candidate selector or scope, then use the condition that actually distinguishes the intended target. Do not assume uniqueness without checking the page.
A selector cannot reach a component’s target. The target may be in an iframe or shadow root, or the selector syntax may not express that boundary. Use the owning frame’s locator for an iframe. For open shadow roots, consider Puppeteer’s documented shadow combinators or text selector behavior, while observing their limits.
A lower-level handle remains allocated. An ElementHandle was obtained and not disposed. Dispose the handle when finished. Prefer a locator when its retrying interaction behavior fits the task.

8. Performance, reliability, and cost

Filtering adds a browser-side condition to a locator operation. Keep the initial selector narrow and the predicate simple so the intended condition is easy to understand and evaluate. The cited Puppeteer documentation provides no benchmark for locator filtering, so do not infer a speed advantage over another selector strategy from the API alone.

For reliability, use selectors tied to stable page meaning, scope to the correct page or frame, serialize Node values safely, and account for the target’s rendered state. Retries help with changing pages, but they do not make an incorrect selector or predicate correct. Puppeteer’s documentation describes software behavior, not a per-operation service price; runtime and hosting costs depend on where and how your automation runs.

9. When a locator is not enough

Puppeteer identifies lower-level alternatives such as page.waitForSelector() and ElementHandle when an API does not provide an operation you need. waitForSelector() is lower-level and does not automatically retry an action after that action fails. If you use a returned element handle, dispose of it when finished to avoid memory leaks.

Or skip the browser setup

If your goal is to capture a page rather than interact with its controls, ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns a PNG, JPEG, WebP, or PDF. 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,
)
r.raise_for_status()
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. An MCP server lets AI agents use screenshot, page information, and PDF capture tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does .filter() return an array?

No. It refines a locator expectation and participates in locator retries.

Can the predicate use a Node.js closure?

No. The predicate runs in the browser context. Serialize values from Node when building the predicate string.

Does filtering guarantee one match?

No uniqueness guarantee is established by the documented filter behavior. Make the candidate selector and condition appropriately specific for your page.