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

A CSS selector in Puppeteer is just a JavaScript string. Store it in a variable and pass that variable directly to a selector-taking method such as page.$(), page.$eval(), or page.waitForSelector():

const selector = '.result';
const element = await page.$(selector);

Do not put the variable name in quotes. page.$(selector) uses the selector’s value; page.$('selector') searches for an element literally matching the word selector.

Pass the selector variable directly

Most Puppeteer methods that select elements expect a selector as their first argument. A function parameter works exactly like any other string value:

import puppeteer from 'puppeteer';

async function findResult(page, selector) {
  return page.$(selector);
}

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');

const result = await findResult(page, '.result');
console.log(result ? 'Found the element' : 'No match');

await browser.close();

Here, selector is a parameter containing '.result'. Puppeteer receives that string at runtime and interprets it as a selector.

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

Do not quote the parameter name

async function getElement(page, selector) {
  const correct = await page.$(selector);       // Uses the parameter value
  const incorrect = await page.$('selector');   // Searches for a literal selector named "selector"
  return { correct, incorrect };
}

Use quotes only when you are writing a literal selector directly, such as page.$('.result').

Choose the Puppeteer method for the job

Method Call shape Waits? When there is no match Result
page.$ page.$(selector) No Resolves to null First matching ElementHandle
page.$eval page.$eval(selector, callback) No Throws an error Callback’s returned value
page.waitForSelector page.waitForSelector(selector, options) Yes Throws after the timeout ElementHandle
page.evaluate page.evaluate(callback, selector) No, unless your callback waits Your callback decides Serializable value returned by the page function

The official Puppeteer API references document these signatures for the current documentation, including the Page.$eval and Page.evaluate pages identified as version 25.12.0. APIs can differ across historical releases, so check the version installed in your project.

Use a parameter with page.$()

Use page.$(selector) when you need a handle to one element and the element might be optional. It resolves to null when no element matches.

async function getOptionalCard(page, selector) {
  const card = await page.$(selector);
  if (!card) {
    return null;
  }

  const text = await card.evaluate(node => node.textContent?.trim() ?? '');
  await card.dispose();
  return text;
}

const title = await getOptionalCard(page, '.card-title');

Dispose an ElementHandle when you no longer need it, especially in loops or long-running workers.

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

Use a parameter with page.$eval()

page.$eval() selects the first matching element, passes it as the callback’s first argument, and returns the callback result. Its argument order is selector, callback, then any additional callback arguments.

async function readText(page, selector) {
  return page.$eval(selector, element => element.textContent?.trim() ?? '');
}

const text = await readText(page, '.result');
console.log(text);

If the selector matches nothing, $eval throws. Catch that error when a missing element is an expected condition:

async function readOptionalText(page, selector) {
  try {
    return await page.$eval(selector, element => element.textContent?.trim() ?? '');
  } catch (error) {
    if (error instanceof Error && /failed to find element/i.test(error.message)) {
      return null;
    }
    throw error;
  }
}

Forward extra arguments separately

Arguments after the callback are not additional selector arguments. They are values delivered to the callback:

async function readAttribute(page, selector, attributeName) {
  return page.$eval(
    selector,
    (element, name) => element.getAttribute(name),
    attributeName,
  );
}

const href = await readAttribute(page, 'a.download', 'href');

Puppeteer supplies the matched element first; attributeName arrives as the callback’s second parameter.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Use page.evaluate() when the query belongs in page code

With page.evaluate(), the first argument is a function. Values after that function are serialized and passed into its parameters. The selector is therefore an evaluation argument, not a Puppeteer selector argument:

async function readTextInPage(page, selector) {
  return page.evaluate(
    sel => document.querySelector(sel)?.textContent?.trim() ?? null,
    selector,
  );
}

const value = await readTextInPage(page, '.result');

This approach is useful when you need several DOM operations in one page-context function. The selector is evaluated by document.querySelector, so an invalid CSS selector causes a browser-side exception.

Choosing between $eval and evaluate

  • Use $eval when Puppeteer should perform the selection and you need one matched element.
  • Use evaluate when the entire query and transformation should run inside page JavaScript, or when you need a custom no-match result.
  • Use $$eval (the same selector-first idea) when you need all matching elements and want to map them to serializable data.

Wait for a dynamic element before using the parameter

If the page renders the target later, call page.waitForSelector(selector, options). The documented default timeout is 30,000 milliseconds. You can require visibility, wait for hiding, set a different timeout, or provide an abort signal.

async function readLoadedResult(page, selector) {
  await page.waitForSelector(selector, {
    visible: true,
    timeout: 10_000,
  });
  return page.$eval(selector, element => element.textContent?.trim() ?? '');
}

const result = await readLoadedResult(page, '[data-testid="result"]');

Waiting and extraction are separate operations: waitForSelector confirms the desired state, then $eval reads the element. For interactions, Puppeteer’s locator APIs provide automatic waiting for presence and an appropriate element state; the page-interactions guide describes when locators are preferable to lower-level handles.

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

Validate and build selectors safely

Reject invalid or empty input

function requireSelector(value) {
  if (typeof value !== 'string' || value.trim() === '') {
    throw new TypeError('selector must be a non-empty string');
  }
  return value;
}

async function hasElement(page, selector) {
  const safeSelector = requireSelector(selector);
  return (await page.$(safeSelector)) !== null;
}

Validation gives callers a clear error before Puppeteer reports a malformed query.

Escape dynamic text used inside a CSS selector

If a parameter is a value inside a selector rather than the complete selector, escape it before interpolation. Modern browsers expose CSS.escape() in page context:

async function findById(page, id) {
  return page.evaluate(rawId => {
    const escaped = CSS.escape(rawId);
    return document.querySelector(`#${escaped}`)?.textContent ?? null;
  }, id);
}

Prefer stable attributes such as data-testid. Avoid constructing selectors from untrusted input unless you validate or escape it; malformed selectors can fail the operation, and broad selectors can target the wrong element.

Remember that Puppeteer supports more than CSS

CSS examples use syntax such as .result, #login, and button[type="submit"]. Puppeteer also documents additional selector forms, including text, accessibility role/name, and XPath. Label a selector according to the syntax it uses so a function’s contract is not misleading.

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

Common mistakes and fixes

  • Callback in the selector position: page.$eval(() => ..., selector) is reversed. Use page.$eval(selector, element => ...).
  • Literal variable name: replace 'selector' with selector unless the literal word is genuinely the query.
  • Wrong callback parameter: in $eval, the first callback parameter is the matched element. In evaluate, parameters after the function receive the values you pass.
  • Unexpected null: $ does not wait. Check the URL, frame, selector spelling, and page timing, or wait first.
  • Unexpected exception: $eval throws for no match. Use $ for optional elements or catch the specific failure.
  • Invalid selector: inspect punctuation, escaping, and quotes. Test the same string with document.querySelector() in DevTools.
  • Wrong frame: selectors are scoped to a page or frame. Obtain the correct frame and call its selector methods there.
  • Stale handle: a navigation or re-render can detach an ElementHandle. Re-select after the DOM changes instead of retaining handles indefinitely.

Reliable reusable helper patterns

Return a default for an optional value

async function textOrDefault(page, selector, fallback = '') {
  const node = await page.$(selector);
  if (!node) return fallback;
  try {
    return await node.evaluate(element => element.textContent?.trim() ?? '');
  } finally {
    await node.dispose();
  }
}

Wait, then click

async function clickWhenReady(page, selector) {
  await page.waitForSelector(selector, { visible: true, timeout: 30_000 });
  await page.click(selector);
}

For complex interactions, a locator can reduce manual waiting and handle state checks for you.

Performance, reliability, and debugging

  • Pass one selector value through helpers rather than repeatedly rebuilding identical strings.
  • Prefer one $eval or $$eval that returns the needed serializable data over transferring many element handles.
  • Use explicit, realistic timeouts. A long timeout can hide a broken selector; a short timeout can fail on a slow page.
  • Log the final selector value, URL, frame URL, and timeout when diagnosing failures. Do not log secrets embedded in surrounding page data.
  • After navigation, route changes, or framework re-renders, wait for a stable selector and then query again.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean website image rather than DOM automation, ScreenshotNeo provides a single screenshot request. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the complete parameter list in the ScreenshotNeo documentation. This cURL request saves a WebP image:

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()
open("shot.webp", "wb").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 failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo includes full-page and element capture, 12 device presets plus custom viewports, retina scale, dark mode, PDF controls, custom CSS and JavaScript, click and wait conditions, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, selectable cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.

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

The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; all features are available on every plan, and yearly billing provides two months free. Sign up for the free ScreenshotNeo plan.

Frequently Asked Questions

Does Puppeteer require a special syntax for a selector parameter?

No. A selector is an ordinary string. Pass the variable directly to the method that accepts a selector.

What is the difference between a selector argument and an evaluate argument?

In $eval, the selector is the first Puppeteer argument. In evaluate, values after the page function are delivered to that function as its parameters.

Which method should I use when an element may not exist?

Use page.$(selector) and test for null. Use $eval only when a match is required, or catch its no-match error.

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

Why does my selector work in DevTools but fail in Puppeteer?

Check that Puppeteer is querying the same frame and page state, that the selector is valid CSS, and that the element has appeared before the query runs.

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.