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

Use a Puppeteer locator and .filter() when you want to find an element and interact with it. If you specifically need persistent ElementHandle objects, query with page.$$() (or a container handle’s $$()) and test each returned handle in Node.js. If you only need text, attributes, or other values, use page.$$eval() instead.

Those approaches solve different problems: locators are the recommended starting point for interaction, handles let you retain references to page elements, and $$eval() returns data rather than handles. The examples below show how to choose, filter safely, and clean up handles.

Choose the right Puppeteer API

Start by deciding what you need after the selection. Puppeteer recommends locators for selecting elements to interact with; ElementHandle and waitForSelector are lower-level options for cases where locator functionality is not sufficient.

What you need Approach What you get
Find a candidate and click, type, or otherwise interact with it page.locator(selector).filter(predicate) A locator that can be used for an interaction. Locator actions can wait for relevant conditions.
Keep references to several matching elements page.$$(selector), then evaluate a predicate for each handle An array of ElementHandle objects you must manage and dispose.
Read text, attributes, or other serializable values page.$$eval(selector, callback) The callback’s returned value, not a persistent collection of handles.
Run custom page-side selection and retain its result page.evaluateHandle(callback) A handle to the returned page object; dispose it when finished.

Prefer a selector or locator when it expresses the selection clearly. Puppeteer’s selector syntax includes CSS and additional forms such as text, accessibility selectors, XPath, and open Shadow DOM traversal; consult the guide for the syntax supported by your installed version.

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

Filter a locator and interact with the match

For a button whose text must match exactly, use a locator filter. The predicate runs in the browser page context, so it can inspect the candidate element but cannot directly access ordinary Node.js variables.

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

Locator actions can automatically wait for conditions relevant to the action. For clicking, Puppeteer’s guide describes checks involving viewport position, visibility, enabled state, and bounding-box stability. This can make a locator a better fit than immediately collecting handles when the actual goal is simply to click the right button.

Use a dynamic value in a locator predicate

To compare against a value defined in Node.js, serialize it into the predicate function string rather than expecting a callback to close over the local variable:

const buttonName = 'My button';

await page
  .locator('button')
  .filter(`button => button.textContent === ${JSON.stringify(buttonName)}`)
  .click();

JSON.stringify() produces a JavaScript string literal for this example, including escaping characters that could otherwise break the generated expression. Keep the value as data; do not build a predicate by concatenating untrusted code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Get and filter actual ElementHandles

Use page.$$() when you need an array of references to every element matching a selector. It returns an array of ElementHandle objects. Evaluate each candidate in the page context, passing the expected value as an argument, and retain only the matches:

const handles = await page.$$('button');
const matchingHandles = [];

for (const handle of handles) {
  const matches = await handle.evaluate(
    (button, expectedName) => button.textContent === expectedName,
    'My button',
  );

  if (matches) {
    matchingHandles.push(handle);
  } else {
    await handle.dispose();
  }
}

// matchingHandles now contains the matching ElementHandles.
// Use them for the work that requires retained element references.
for (const handle of matchingHandles) {
  await handle.click();
  await handle.dispose();
}

The callback passed to handle.evaluate() runs against the element in the browser context. The expected label is supplied as an argument, so it does not rely on a Node.js closure. In this example, the exact comparison deliberately treats whitespace and letter case as significant. If the page contains extra whitespace, choose an explicit normalization rule, such as trimming the text, and use that same rule for the expected label.

Dispose of non-matches and matches

An ElementHandle keeps its DOM element from being garbage-collected while the handle remains active. Dispose of rejected handles as soon as you know they are not needed, and dispose of retained handles when their work is complete. The loop above disposes rejected handles immediately; its final loop disposes each retained handle after clicking it.

If an operation can fail after some handles have been collected, put cleanup in a finally block so retained references are released even when an action throws:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const handles = await page.$$('button');
const matchingHandles = [];

try {
  for (const handle of handles) {
    const matches = await handle.evaluate(
      (button, expectedName) => button.textContent === expectedName,
      'My button',
    );

    if (matches) {
      matchingHandles.push(handle);
    } else {
      await handle.dispose();
    }
  }

  for (const handle of matchingHandles) {
    await handle.click();
  }
} finally {
  await Promise.all(
    matchingHandles.map(handle => handle.dispose()),
  );
}

This cleanup assumes the rejected handles were already disposed in the loop. If you add code that may throw before all candidates have been classified, track every acquired handle and ensure the cleanup path covers those that remain undisposed. A navigation or destruction of the parent execution context also causes automatic disposal, but explicit cleanup makes the intended lifetime clear.

Scope a query to a container

If the target buttons belong to one known region, first locate that region and query within its handle. The scoped query avoids collecting matching buttons elsewhere on the page:

const container = await page.$('#results');

if (!container) {
  throw new Error('Results container was not found');
}

const buttons = await container.$$('button');

for (const button of buttons) {
  // Evaluate a predicate or use the handle here.
  await button.dispose();
}

await container.dispose();

Check for a null container before calling its query method. Each returned button handle and the container handle have separate lifetimes, so dispose of both when they are no longer needed.

Return values instead of handles with $$eval

If the goal is to collect labels, there is no need to retain element references. page.$$eval() passes all matching DOM nodes to a callback in the page context and resolves to the callback’s result:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
const labels = await page.$$eval('button', buttons =>
  buttons
    .filter(button => button.textContent === 'My button')
    .map(button => button.textContent),
);

console.log(labels);

The returned result should be data that can be passed back from the page context, such as strings or arrays of values. It is not an array of durable ElementHandle objects. Use this approach for extraction; use locator interaction or handles when you must act on the actual page elements.

Get a handle from a custom selection

When a custom page-side expression selects one element and you need to keep an element reference, use page.evaluateHandle(). For example:

const button = await page.evaluateHandle(() =>
  document.querySelector('button'),
);

try {
  await button.click();
} finally {
  await button.dispose();
}

When the in-page callback returns an element reference, Puppeteer provides an ElementHandle. By contrast, page.evaluate() returns the evaluated value and is appropriate when you want data rather than a retained page object. In TypeScript, the API reference notes that a generic can be supplied when you know the return value is an ElementHandle.

Common problems and fixes

  • The filter cannot see a Node.js variable: locator filters execute in the browser context. Embed a serialized value in the function string, as shown above, or pass values as arguments to an evaluation callback.
  • The selected text does not match: exact string comparison distinguishes whitespace and case. Inspect the actual text, then choose whether to compare exactly, trim whitespace, or use another deliberate matching rule.
  • The container lookup is null: the selector did not find a matching element at the time of the query. Check the selector and page state, and handle the null result before calling container.$$().
  • A handle is detached or its context is gone: the page may have replaced the node, navigated, or destroyed the execution context between selection and action. Re-query against the current page state; for ordinary interaction, consider a locator that can wait for action conditions.
  • Handles accumulate during a long task: dispose of non-matches immediately and dispose of matches after use. A retained handle keeps a page object alive until it is disposed or its parent context is destroyed.
  • You only need text or attributes: use $$eval() and return the data. Keeping handles adds lifecycle work without providing a benefit if no later action needs the elements.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Version and behavior notes

Puppeteer’s official documentation pages identified for these APIs carry different version labels: the interactions guide and Page evaluation references are labeled 25.12.0, the ElementHandle class reference 25.10.0, and the $$ and $$eval references 25.9.0. Those labels are not a single shared version guarantee. Check the documentation corresponding to the Puppeteer version installed in your project before relying on version-specific details.

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

Or skip the browser setup

If your goal is a screenshot rather than inspecting or interacting with individual DOM elements, ScreenshotNeo takes a screenshot through one GET request. It can accept cookie or consent banners and remove known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. An MCP server provides screenshot tools for AI agents and other MCP clients.

For example, with cURL:

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

See the ScreenshotNeo API documentation for request options. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

Frequently Asked Questions

Does page.$$() return ElementHandles or DOM nodes?

It returns an array of Puppeteer ElementHandle objects for elements matching the selector.

Can I pass a Node.js variable directly into a locator filter callback?

No. The callback runs in the page context. Serialize a value into a function string or pass it as an argument to an evaluation callback.

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

What should I use if I need to keep only one result from a custom page-side query?

Use evaluateHandle() for the returned page object, then dispose of the resulting handle when finished.

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.