ScreenshotNeo

BlogHow-to

How to Pass a Function Parameter as a CSS Selector in Puppeteer

Pass dynamic CSS selectors to Puppeteer correctly with runnable JavaScript, waiting patterns, error handling, and reusable helpers.

By the ScreenshotNeo team30 September 202610 min read

How to Pass a Function Parameter as a CSS Selector in Puppeteer

In Puppeteer, a CSS selector is an ordinary JavaScript string. Store it in a variable or receive it as a function parameter, then pass that value directly to a selector-taking method such as page.$(), page.$eval(), or page.waitForSelector().

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

Do not add another layer of quoting around the variable. page.$(selector) uses the selector stored in the variable; page.$('selector') searches for an element literally matching the tag or selector text selector.

Pass the selector as a function parameter

A helper function can accept a selector exactly like any other argument. This keeps browser logic reusable across pages and components.

A selector value can flow through a helper into Puppeteer's query or evaluation method.
A selector value can flow through a helper into Puppeteer's query or evaluation method.
import puppeteer from 'puppeteer';

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

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

  const selector = 'h1';
  const element = await findElement(page, selector);

  console.log(element ? 'Found the element' : 'No match');
  await browser.close();
}

main().catch(console.error);

The parameter name does not matter. What matters is that the value is a valid selector string at runtime. You can pass a class, ID, attribute selector, descendant selector, or any other CSS selector supported by the page.

await findElement(page, '#checkout');
await findElement(page, '.card[data-state="ready"]');
await findElement(page, 'main article h2');

Choose the Puppeteer method for the job

Puppeteer offers several APIs that accept or receive a selector. Their waiting behavior, return values, and missing-element behavior differ.

Method Use it for When no element matches Return value
page.$(selector) Find one element handle Resolves to null ElementHandle or null
page.$eval(selector, callback) Read or change the first matching element Throws Callback result
page.waitForSelector(selector, options) Wait for an element to appear or change state Throws after timeout ElementHandle
page.evaluate(callback, selector) Run a DOM query inside page context Your callback decides Serializable callback result

Use page.$() when the element is optional

page.$() resolves to null if there is no match, so it is suitable for optional UI such as a dismiss button or an advertisement slot.

const selector = '.optional-panel';
const panel = await page.$(selector);

if (panel) {
  await panel.click();
  await panel.dispose();
}

Dispose handles when you no longer need them, especially in long-running jobs that inspect many pages.

Use page.$eval() for a one-off value

page.$eval() takes the selector first and the callback second. Puppeteer finds the first matching node and passes that node to the callback as its first parameter.

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

const title = await readText(page, 'h1');
console.log(title);

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

async function readOptionalText(page, selector) {
  try {
    return await page.$eval(selector, element => element.textContent?.trim() ?? '');
  } catch (error) {
    if (String(error).includes('failed to find element')) return null;
    throw error;
  }
}

Use waitForSelector() when the page is asynchronous

Pages often render after an API request or client-side route change. Pass the dynamic selector to waitForSelector and configure the wait to match the state you need. Puppeteer’s documented default timeout is 30 seconds. The API supports options including visible, hidden, timeout, and signal. See the official waitForSelector reference.

const selector = '[data-testid="results"]';
await page.waitForSelector(selector, {
  visible: true,
  timeout: 10_000,
});

const text = await page.$eval(selector, element => element.textContent);

Set hidden: true when you need a loading mask or modal to disappear:

await page.waitForSelector('.loading-spinner', {
  hidden: true,
  timeout: 15_000,
});

Understand selector arguments versus evaluation arguments

Two valid patterns can look similar but have different argument order.

Waiting for the right page state prevents no-match errors on dynamic sites.
Waiting for the right page state prevents no-match errors on dynamic sites.

Selector-taking API

With $eval, Puppeteer receives the selector as the first API argument and the callback as the second:

const selector = '.result';
const value = await page.$eval(selector, element => element.textContent);

Any arguments after the callback are forwarded to that callback. They are not additional selectors.

const suffix = ' — ready';
const label = await page.$eval(
  '.result',
  (element, extra) => `${element.textContent?.trim()}${extra}`,
  suffix,
);

Evaluation API

page.evaluate() runs a function in the page context. The selector is an argument after the callback, then arrives as the callback’s parameter.

const selector = '.result';
const value = await page.evaluate(
  sel => document.querySelector(sel)?.textContent?.trim() ?? null,
  selector,
);

This form is useful when the query is part of a larger DOM operation. Values passed into evaluate must be serializable; a selector string is safe. The official Page.evaluate documentation describes how additional arguments are delivered to the evaluated function.

Build reusable helpers safely

Keep selector validation, waiting, and extraction separate so callers can choose the behavior they need.

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

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

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

Validation catches accidental undefined, empty strings, and non-string values before Puppeteer reports a less useful error. It does not prove that a selector is valid CSS or that it will match the current document. For user-supplied selectors, catch syntax errors and apply your own allowlist if your application needs to restrict queries.

Dynamic selectors and escaping

When a selector contains a value such as an ID supplied at runtime, escape that value before concatenating it into CSS. The browser provides CSS.escape() in page context, while Node.js code can use a CSS escaping package or avoid concatenation by using a stable attribute structure.

const itemId = 'item:42';
const selector = `[data-item-id="${itemId.replaceAll('"', '\\"')}"]`;
const item = await page.$(selector);

Prefer selectors based on stable attributes such as data-testid. Avoid depending on generated class names, deep positional chains, or visual text that changes with localization.

Interactions with a parameterized selector

For a click or form operation, wait for the selector, then interact with the handle or locator. Puppeteer’s page interactions guide covers automatic waiting and locator-based interactions; see Page interactions.

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

await clickWhenReady(page, 'button[type="submit"]');

For newer interaction code, locators can be preferable because they wait for presence and an appropriate interaction state. Use a selector-taking locator where available, and keep the selector in a variable or helper parameter just as you would with page.click.

Full runnable example

The following script accepts a selector from the command line, waits for it, extracts text, and handles an optional match.

import puppeteer from 'puppeteer';

async function extract(page, selector) {
  if (!selector || typeof selector !== 'string') {
    throw new TypeError('Pass a CSS selector string');
  }

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

const selector = process.argv[2] ?? 'h1';
const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  console.log(await extract(page, selector));
} finally {
  await browser.close();
}

Run it with node extract.mjs 'h1'. A selector containing spaces or brackets must be quoted by your shell.

Common errors and fixes

Error or symptom Cause Fix
No element found $eval ran before the element existed, or the selector is wrong. Verify the selector in DevTools, then wait with waitForSelector or use $ for optional content.
Always searches for “selector” The variable name was quoted. Use page.$(selector), not page.$('selector').
Timeout exceeded The element never appeared, was inside a different frame, or the timeout is too short. Check navigation and frame context, wait for the relevant state, and set a justified timeout.
Invalid or unexpected token The CSS selector has invalid syntax or unescaped runtime data. Test the selector in the browser console and escape dynamic values.
ElementHandle is detached A framework replaced the node after you found it. Locate the selector again immediately before the action, or use a locator.
Works in the main page but not an iframe The target belongs to a child frame. Find the frame, then call frame.$(selector) or frame.waitForSelector(selector).
Text is empty Content is rendered later, hidden, or stored in a property rather than text nodes. Wait for the rendered state and inspect the element’s attributes or input value.

Frames, shadow DOM, and special selector syntax

A selector is evaluated in a document context. For an iframe, use the corresponding Frame object:

const frame = page.frames().find(f => f.url().includes('/checkout'));
if (!frame) throw new Error('Checkout frame not found');
await frame.waitForSelector('#card-number');
const value = await frame.$eval('#card-number', element => element.value);

Shadow DOM boundaries may require shadow-root-aware queries or Puppeteer’s supported selector syntax. Puppeteer also supports selector forms beyond CSS, including text, accessibility role/name, and XPath forms. Label examples as CSS only when they are CSS; do not assume every Puppeteer selector string follows CSS grammar. The Page API reference and interactions guide document the current supported forms.

Performance, reliability, and cost considerations

  • Prefer one targeted query over repeatedly scanning the entire DOM in evaluate.
  • Wait for a meaningful state instead of using a long fixed delay. This reduces idle time on fast pages while allowing slower pages to finish.
  • Reuse a browser and page when processing many URLs, but clear cookies and state between tenants or jobs.
  • Dispose element handles and close pages in finally blocks so failures do not leak resources.
  • Set explicit navigation and selector timeouts. Infinite waits make failed jobs difficult to recover.
  • Retry only transient navigation or rendering failures. Repeating an invalid selector wastes time and can hide a programming error.

Selector calls themselves do not have a separate Puppeteer fee. Your practical cost comes from browser CPU, memory, hosting, proxy or bandwidth usage, and the time spent waiting for pages. Instrument navigation duration, selector wait duration, and no-match counts so you can identify slow or unstable targets.

Or skip the browser setup

If your goal is a clean screenshot rather than custom DOM interaction, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether the shot was billed.

cURL:

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}`);
const body = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', body));

See the ScreenshotNeo documentation for request options. The API supports full-page capture with lazy images loaded, element capture by CSS selector, dark mode, device presets or custom viewports, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs, usage data, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account and start with the monthly free allowance.

FAQ

Can I pass a selector through several helper functions?

Yes. Keep it as a string parameter and pass it unchanged until it reaches the Puppeteer method that consumes it.

Should I use $eval or evaluate?

Use $eval when Puppeteer should select the element for you. Use evaluate when the selector is part of a larger function that runs in the page context.

What happens if the selector matches multiple elements?

$ and $eval operate on the first match. Use $$ or $$eval when you need all matches.

Is a Puppeteer selector always CSS?

No. Puppeteer also supports additional selector syntax such as text, accessibility queries, and XPath forms. Describe and validate the syntax your code actually uses.

Why does my selector work manually but fail in automation?

The automated page may be in a different frame, before client rendering completes, logged out, or showing a different responsive layout. Capture the URL, viewport, frame tree, and page HTML at failure time.