ScreenshotNeo

BlogHow-to

How to Add a Custom Query Handler in Puppeteer

Register a Puppeteer custom query handler with queryOne and queryAll, use it in modern selectors, and troubleshoot version and selector pitfalls.

By the ScreenshotNeo team4 October 20267 min read

Register a named handler with Puppeteer.registerCustomQueryHandler(name, handler). Implement queryOne to return the first match and queryAll to return all matches. Use the handler with Puppeteer’s current pseudo-element selector syntax, ::-p-name(argument); this syntax can be composed with other selectors and is the recommended form in the current guide.

1. Install Puppeteer and register a handler

This runnable ES module example creates a handler that finds elements by ID. The handler name contains only Latin letters, as required by the API reference.

import puppeteer, { Puppeteer } from 'puppeteer';

Puppeteer.registerCustomQueryHandler('byId', {
  queryOne: (elementOrDocument, id) => {
    return elementOrDocument.querySelector(`#${CSS.escape(id)}`);
  },
  queryAll: (elementOrDocument, id) => {
    return elementOrDocument.querySelectorAll(`#${CSS.escape(id)}`);
  },
});

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setContent(`
    <main>
      <button id="save:changes">Save</button>
      <button id="save:copy">Save a copy</button>
    </main>
  `);

  // queryOne is used by an action locator: click the first matching element.
  await page.locator('::-p-byId(save:changes)').click();

  // $$ returns all matches for the selector.
  const matches = await page.$$('::-p-byId(save:copy)');
  console.log(`Found ${matches.length} matching element(s)`);
  await Promise.all(matches.map((element) => element.dispose()));
} finally {
  await browser.close();
}

Save this as custom-query-handler.mjs and run node custom-query-handler.mjs. Install Puppeteer in the project first with npm install puppeteer. Puppeteer’s package normally downloads a compatible browser; if your project manages Chrome separately, consult the installation guidance for your installed Puppeteer version. See the handler API reference and page interactions guide.

2. Understand the handler contract

The registration call takes a handler name and an object of query callbacks. Each callback receives an element or document in the page context and the selector argument inside the parentheses.

Callback Expected result Use
queryOne(elementOrDocument, selector) One matching element, or null Locators and single-element queries
queryAll(elementOrDocument, selector) A collection of matching elements Queries that return every match

You can implement only the callback your handler needs. For an action such as clicking one match, queryOne is the relevant operation. Implement both when consumers need both single and multiple matches. The callback executes against page DOM objects; do not assume variables from Node.js scope are available inside it.

Return DOM matches rather than performing automation actions inside the query callback. Use a locator for interaction: locators wait for the target and apply actionability checks. Puppeteer’s guide recommends locators for selecting and interacting with elements.

3. Choose selector syntax

Current pseudo-element form

Write ::-p-<name>(<argument>), substituting the registered handler name. For the example above, the selector is ::-p-byId(save:changes). The handler name must match registration.

Custom selectors can be composed with CSS selectors. For example, to find a matching component only inside a sidebar:

const sidebarSave = page.locator('.side-bar ::-p-byId(save:changes)');
await sidebarSave.click();

Use this syntax for new code because the current page-interactions guide demonstrates it for custom handlers and selector composition.

Legacy prefixed form

The API reference also documents the older name/selector prefix form. It is legacy syntax, and the current guide describes it as limited to one non-CSS selector at a time; it cannot be composed with multiple selectors in the same way. Keep it only where compatibility with existing code requires it, and check the documentation matching your installed version before migrating.

4. Build a useful custom query

A custom handler is useful when a page exposes a stable, meaningful way to identify elements that ordinary CSS selectors do not express conveniently. Keep the handler’s argument contract small and explicit. This example accepts an ID, escapes it for safe use in a CSS selector, and supports both one and all matches.

For a React or Vue component handler, you might inspect framework-specific component metadata, but that couples automation to framework internals. Those details can change between framework versions. Prefer stable DOM attributes or test IDs when the application can provide them; use framework internals only when you can pin and maintain the integration.

Avoid building selectors with unescaped user-provided values. CSS metacharacters in IDs or other values can change selector meaning or make parsing fail. CSS.escape handles values used as CSS identifiers in the example. If your argument is a more complex expression, define and validate its grammar rather than inserting it into a selector string unchecked.

5. Troubleshoot common problems

Symptom Likely cause Fix
“Unknown handler” or selector does not resolve The selector name and registered name differ, or registration has not run in this process. Register before querying and make the spelling and case identical in both places.
Handler name is rejected The API reference restricts names to upper- and lower-case Latin letters. Use a name such as byId or reactComponent; avoid hyphens and punctuation.
No element is returned The selector argument does not match the DOM, the target has not rendered, or the callback searches a different subtree than expected. Inspect the rendered DOM, verify the argument, and use a locator so Puppeteer can wait for the target when appropriate.
Syntax error for IDs containing punctuation The raw argument was inserted into a CSS selector without escaping. Escape identifier values with CSS.escape or use a DOM lookup strategy that treats the value literally.
Works in one Puppeteer version but fails in another Selector APIs and handler examples differ across documentation versions; older deprecated handler functions were removed in Puppeteer 23.0.0. Check the guide and API reference for the installed version, then migrate away from removed deprecated functions.
Framework component selector breaks after an upgrade The handler depends on private component or virtual-DOM internals. Use stable application attributes where possible, or pin and deliberately maintain the framework-specific integration.
Node variable is undefined inside the callback The callback runs in the page context, separate from the Node.js execution context. Pass data through the handler’s selector argument or use supported page evaluation mechanisms outside the query callback.

6. Performance and reliability

  • Keep queries scoped. Composing a CSS ancestor selector with the custom selector can limit where the handler searches.
  • Prefer a direct DOM lookup for a simple stable key, as in the ID example, over traversing framework internals.
  • Do not repeatedly run broad queryAll operations when a single match is enough. Use the single-match callback and a locator for actions.
  • Make handlers deterministic: the same DOM and argument should identify the same set of elements. Avoid depending on transient generated class names or undocumented framework state.
  • Register handlers once during process setup, before pages use the selector. Keep the registration close to the automation code that owns the selector contract.
  • For debugging, first test the underlying DOM query in the page, then verify the custom selector name and argument, and finally verify the action or locator behavior.

No handler-specific cost or benchmark is established in the cited documentation. In practice, the main reliability cost is maintenance when a handler depends on a DOM or framework detail that changes.

7. Screenshot a page while debugging a selector

A screenshot can help inspect layout-dependent states while you debug browser automation, though it does not replace checking the DOM or selector result. If you want to capture a page without installing and running a browser for that step, ScreenshotNeo is a website screenshot API and MCP server for developers.

Or skip the browser setup

Make one GET request to capture a page as an image. See the ScreenshotNeo API documentation for parameters and options.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status} ${res.statusText}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', image));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

8. Frequently asked questions

Can one handler support more than one custom query?

Yes. The handler receives its argument as a string, so you can define a small argument format and interpret it in the callback. Validate that format and document it for callers.

Does a custom query handler click or wait for an element?

No. It defines how Puppeteer finds matches. Use the returned match with a locator or another interaction API for actions and waiting behavior.

Should I use a custom handler for every selector?

No. Puppeteer supports CSS and additional built-in selector syntax. Add a custom handler when it gives your code a clear, reusable selector contract.

Where should I check exact compatibility?

Use the official guide and API reference for the version installed in your project. The researched API page identifies version 25.3.0, while the current interactions guide identifies 25.12.0, so version-specific details may differ.

Official references