How to Find an Element in Puppeteer
Use Puppeteer locators to find and interact with elements, or choose a query or explicit wait when you need a handle or extracted value.
For most Puppeteer interactions, use page.locator(selector). It waits for the element to be ready for the requested action and retries when the action cannot proceed yet. For an immediate query, use page.$(); for an explicit wait, use page.waitForSelector(); and for reading values, use page.$eval() or page.$$eval().
The examples below use Puppeteer’s JavaScript API. See the official page interactions guide and getting started guide.
1. Set up a runnable Puppeteer example
Install Puppeteer in a new Node.js project:
npm install puppeteer
Save this as find-element.js and run it with node find-element.js. It opens a page, finds a button by CSS selector, clicks it, reads the result, and closes the browser.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const heading = await page.locator('h1').waitHandle();
const text = await page.evaluate(element => element.textContent?.trim(), heading);
console.log(text);
await heading.dispose();
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The sample uses a locator to find the heading, then reads its text in the page context. To click or fill an element, use locator actions directly, as shown below. If you use TypeScript with a property specific to an element, narrow its type (for example, to HTMLInputElement).
2. Find and interact with an element using a locator
Puppeteer’s documentation recommends locators for selecting an element and interacting with it. A locator describes how to find the target; its action checks relevant readiness conditions, such as visibility and enabled state, and retries if it cannot perform the action yet. For actions such as clicking, it also waits for a stable bounding box.
await page.locator('button.submit').click();
await page.locator('input[name="email"]').fill('reader@example.com');
Locators are particularly useful when a page renders content asynchronously or changes layout while loading. They do not mean your selector is unique: if multiple elements match, choose a more specific selector that identifies the intended control.
3. Choose a selector that fits the page
CSS selectors are accepted directly. Prefer an ID, name, data attribute, or other selector the page exposes specifically for the element, when available. Puppeteer also provides selector syntax for text, accessible role and name, XPath, and traversal through open shadow roots.
| Target | Example | Use when |
|---|---|---|
| CSS | page.locator('#save-button') |
The page has a useful ID, class, or attribute. |
| Text | page.locator('::-p-text(Save changes)') |
The visible text is a good way to identify a minimal matching element. |
| Accessible role and name | page.locator('::-p-aria([name="Save changes"][role="button"])') |
You want to identify a control by its computed accessible name and role. |
| XPath | page.locator('::-p-xpath(//button[@type="submit"])') |
An XPath expression describes the target more clearly than CSS. |
| Open shadow roots | Use Puppeteer’s shadow DOM selector syntax | The target is inside an open shadow root; consult the selector guide for deep combinators and escaping. |
Text selectors can match the minimal element containing the text. ARIA selectors use the browser’s computed accessible name and role. XPath uses the browser’s native Document.evaluate. Selector punctuation and shadow-root boundaries can affect syntax, so check the official selector guide for the exact expression required by your page. Avoid relying on generated class names or an absolute XPath when a meaningful attribute or accessible name is available.
4. Query one element, many elements, or a value
For elements already in the DOM, Puppeteer offers query methods. These query immediately; they do not wait for future rendering.
// First matching element, or null if there is no match.
const button = await page.$('button.submit');
// Every matching element, or an empty array.
const buttons = await page.$$('button.submit');
// Read a value from the first matching element.
const email = await page.$eval('input[name="email"]', element => element.value);
// Collect text from all matching elements.
const labels = await page.$$eval('li', items =>
items.map(item => item.textContent?.trim())
);
$eval() and $$eval() run the callback in the page context. The first passes the first matching element; the second passes the matching elements together. $eval() throws when there is no match. If absence is expected, use $() and check for null, or wait for the element explicitly first. The callback may read DOM properties such as value or textContent; it should not depend on Node.js variables unless those are passed as arguments.
5. Wait for an element that appears later
Use waitForSelector() when you need an explicit wait for a matching element to enter the DOM. It returns an element handle or throws if the selector does not appear before the timeout. The documented default timeout is 30,000 milliseconds. You can set a page default timeout or override it for one wait.
const result = await page.waitForSelector('.result-card', {
visible: true,
timeout: 10_000,
});
try {
if (result) {
await result.click();
}
} finally {
await result?.dispose();
}
The visible option waits for a visible match; hidden waits for a match to become hidden or detached. The method also accepts a cancellation signal. For example, use an AbortController if your surrounding operation needs to cancel the wait. A handle returned by the wait is a lower-level object: dispose it when finished. Unlike a locator action, the handle does not automatically retry an ensuing click if the page changes between finding and acting.
For a simple action on a dynamic element, prefer page.locator(selector).click() or another locator action. Use waitForSelector() when you specifically need to wait for presence or visibility and work with the resulting handle.
6. Read element properties and HTML
Use $eval() for a property on the first match, or pass an element handle to page.evaluate() for a more involved read. Evaluation runs in the browser page context, and Puppeteer waits if the evaluated function returns a promise.
const heading = await page.$eval('h1', element => element.textContent?.trim());
const body = await page.$('body');
try {
const html = body
? await page.evaluate(element => element.innerHTML, body)
: null;
console.log(html);
} finally {
await body?.dispose();
}
7. Which Puppeteer API should you use?
| Need | API | Behavior |
|---|---|---|
| Find and act, including while the target becomes ready | page.locator(selector) |
Recommended interaction API; checks action readiness and retries. |
| Query one existing match | page.$(selector) |
Returns the first match or null. |
| Query all existing matches | page.$$(selector) |
Returns all matches or an empty array. |
| Wait for presence or visibility | page.waitForSelector(selector, options) |
Waits with visibility, hidden, timeout, and cancellation options. |
| Read or transform one match | page.$eval(selector, fn) |
Runs a page-context function on the first match; throws if absent. |
| Read or transform many matches | page.$$eval(selector, fn) |
Runs one page-context function over all matches. |
8. Troubleshooting
Selector returns null or an empty array
Cause: An immediate query ran before the page added the element, or the selector does not match the current DOM. Fix: Check the live selector and use waitForSelector() or a locator action for content that appears later.
$eval() throws that no element was found
Cause: $eval() requires a match. Fix: Use $() and handle null when missing content is valid, or wait for the selector before evaluating.
A wait times out
Cause: The selector never matched, the page did not reach the state you expected, or rendering took longer than the configured timeout. Fix: Confirm the selector against the actual DOM, check whether the element is in a frame or shadow root, and increase the timeout only when the page legitimately needs more time. Keep a finite timeout so failures are observable.
A click or fill cannot proceed
Cause: The element may be hidden, disabled, covered, moving, or replaced during rendering. Fix: Confirm the selected element is the intended interactive control and use a locator action so Puppeteer can apply its readiness checks and retry.
The selector matches the wrong element
Cause: Several elements share the selector, and first-match APIs choose the first one. Fix: Narrow the selector using a stable attribute, accessible name, or containing region. Use $$() or $$eval() when the task intentionally concerns all matches.
9. Performance, reliability, and cost
Finding an element is usually a small part of browser automation; navigation, JavaScript execution, network conditions, and waiting for dynamic content often dominate elapsed time. Query the smallest useful set of elements and avoid repeatedly scanning all matches when one specific target is enough. Use explicit timeouts and handle expected absence directly instead of allowing a long default wait to hide a selector mistake.
For reliability, prefer selectors that describe the intended element rather than incidental styling, and use locator actions for interaction readiness. Handle navigation and page errors in the surrounding automation, and close the browser in a finally block so failed lookups do not leave Chromium processes running. Puppeteer itself is an open-source browser automation library; its documentation does not publish a per-element query price. Your infrastructure cost depends on where and how long you run the browser.
Or skip the browser setup
If your goal is a screenshot rather than interacting with a page, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its cookie/consent handling accepts banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. AI agents can use its MCP server tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000. Every feature is on every plan.
See the ScreenshotNeo API documentation. Example cURL request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Get 1,000 free screenshots a month with no card.
FAQ
Does page.$() wait for an element?
No. It queries the current DOM and returns the first match or null. Use waitForSelector() to wait explicitly, or a locator action when you intend to interact.
Does Puppeteer return the first match or every match?
page.$() and page.$eval() use the first match. page.$$() and page.$$eval() work with all matches.
Should I use a locator or an element handle?
Use a locator for most interactions. Use an element handle when you need lower-level access to a particular DOM element, and dispose of it when finished.
Can Puppeteer select by accessible name or text?
Yes. Puppeteer supports text and ARIA selector syntax as well as CSS and XPath; see its selector documentation for details and escaping rules.


