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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use a Playwright Locator with a CSS ID selector or Playwright’s explicit ID selector engine:

const saveButton = page.locator('#save-button');
await saveButton.click();

// Equivalent explicit selector engine
const sameButton = page.locator('id=save-button');
await sameButton.click();

Both forms select an element whose HTML id attribute is save-button. Keep the locator and use it for actions or assertions; Playwright can then auto-wait and retry when the element is not ready. The Locator API and other-locators guide document these APIs.

The two ID selectors

Given this markup:

<button id="save-button">Save</button>

the concise CSS form is:

const saveButton = page.locator('#save-button');
await saveButton.click();

The explicit Playwright selector-engine form is:

const saveButton = page.locator('id=save-button');
await saveButton.click();

#save-button is standard CSS syntax and is usually the easiest to read. id=save-button makes it explicit that Playwright’s ID engine is being used. Both target the same HTML attribute and both return a Locator, not a one-time element snapshot.

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

A complete runnable test

This JavaScript example assumes Playwright Test is installed and that the page contains the button:

import { test, expect } from '@playwright/test';

test('saves the form', async ({ page }) => {
  await page.goto('https://example.test/settings');

  const saveButton = page.locator('#save-button');
  await expect(saveButton).toBeVisible();
  await saveButton.click();
  await expect(page.locator('#status')).toHaveText('Saved');
});

For TypeScript, the test is identical apart from the file extension. Use a real URL and IDs from your application. The locator is resolved when an action or assertion runs, so it can wait for a late-rendered button and re-resolve it if the page updates.

Using an ID locator for fields and assertions

A locator is reusable. You can fill a field and then assert its value without performing a separate DOM lookup:

const searchInput = page.locator('#search');
await searchInput.fill('playwright');
await expect(searchInput).toHaveValue('playwright');

Locators are Playwright’s central mechanism for finding elements and provide auto-waiting and retry-ability. Retaining the locator is preferable to storing an element handle that may become stale after a re-render. See the Locator API for the available actions and web-first assertions.

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

Common actions

  • await page.locator('#email').fill('[email protected]') enters text.
  • await page.locator('#menu').click() clicks after Playwright’s actionability checks.
  • await page.locator('#terms').check() checks a checkbox.
  • await page.locator('#avatar').setInputFiles('tests/avatar.png') uploads a file input.
  • await expect(page.locator('#result')).toContainText('Complete') retries until the assertion passes or its timeout expires.

HTML id versus Playwright test ID

An HTML id and a Playwright test ID are different attributes. For this element:

<button id="save-button">Save</button>

use page.locator('#save-button') or page.locator('id=save-button'). Do not use getByTestId('save-button') unless the element also has the configured test-ID attribute.

By default, getByTestId() looks for data-testid:

<button data-testid="save">Save</button>
await page.getByTestId('save').click();

A project can configure another attribute, such as data-pw. That setting changes what getByTestId() searches; it does not turn every HTML id into a test ID. The Page API documents the test-ID behavior.

When to choose each selector

Selector Targets Best use Main caution
#save-button HTML id A stable, unique ID is the intended contract Can be tied to implementation if IDs change during refactors
id=save-button HTML id through Playwright’s ID engine You want the selector engine to be obvious Still depends on the ID being stable and unique
getByRole('button', { name: 'Save' }) Accessible role and name Testing behavior as a user perceives it Requires a correct role and accessible name
getByLabel('Email') Form control associated with a label Inputs with meaningful labels Requires a label relationship or accessible labeling
getByTestId('save') data-testid or the configured test-ID attribute An explicit, deliberate testing contract Does not target an HTML id by default

Playwright recommends user-facing locators such as role, label, and visible text when they represent the behavior under test. Use an ID when that ID is the stable contract you actually intend to verify. For a dedicated testing contract, add a test-ID attribute. The locators guide explains why long CSS and XPath chains can be fragile when DOM structure changes.

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

IDs that need special handling

IDs containing CSS punctuation

CSS selectors give characters such as spaces, colons, periods, brackets, and leading digits special meaning. If an ID is not a simple identifier, either use Playwright’s ID engine or escape the value for CSS:

// ID: user:primary
const user = page.locator('id=user:primary');

// CSS alternative using CSS.escape in the browser context
const id = 'user:primary';
const userByCss = page.locator(`#${CSS.escape(id)}`);

The id=... form avoids having to write CSS escaping rules. If the ID is generated from untrusted data, keep the value in a variable and escape it before constructing a CSS selector.

Duplicate IDs

HTML IDs are intended to be unique. If a page contains two matching elements, an action that requires one target can fail with a strict-mode violation. Fix the markup when possible. If duplicate markup is unavoidable, narrow the locator with a meaningful ancestor or use an explicit occurrence only when the order is part of the contract:

const dialog = page.locator('#settings-dialog');
const saveButton = dialog.getByRole('button', { name: 'Save' });
await saveButton.click();

Using .first() or .nth(1) can hide a defect, so treat it as a conscious choice rather than a default fix.

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

Dynamic IDs

If an ID contains a random suffix on every run, selecting the complete value makes the test brittle. Prefer a stable role, label, text, or test ID. If only a stable prefix or suffix exists, use a CSS attribute selector:

const row = page.locator('[id^="user-"]'); // starts with user-
const item = page.locator('[id$="-details"]'); // ends with -details
const panel = page.locator('[id*="settings"]'); // contains settings

These selectors should still identify one element at the point of action. If they match several items, scope them to a row, dialog, or other stable container.

Frames and shadow DOM

Elements inside an iframe

page.locator('#checkout') searches the main document, not a child frame. Enter the frame with frameLocator() and then apply the same ID selector:

const checkout = page.frameLocator('iframe[name="payment"]');
await checkout.locator('#card-number').fill('4242424242424242');

If the frame is identified by its URL or another stable property, use the corresponding frame locator. The ID remains local to that document.

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

Elements in shadow DOM

Playwright locators generally pierce open shadow roots, so an ID inside an open web component can often be selected directly:

await page.locator('account-settings').locator('#display-name').fill('Ada');

Closed shadow roots are not accessible through ordinary page locators. In that case, test through the component’s public interface, expose a test-friendly attribute, or change the component design; do not depend on private DOM details.

Waiting, visibility, and actionability

An ID matching the DOM does not guarantee that a click can succeed. The element may still be hidden, disabled, covered by another element, or attached to a page that is still rendering. Locator actions wait for the relevant actionability checks, while assertions retry until they pass or time out.

const submit = page.locator('#submit');
await expect(submit).toBeVisible();
await expect(submit).toBeEnabled();
await submit.click();

Do not add arbitrary sleeps as the normal solution. If the application has a meaningful state, wait for that state:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.locator('#results').waitFor({ state: 'visible' });
await expect(page.locator('#results')).toContainText('Ready');

Use a longer, targeted timeout only when the operation legitimately needs it:

await expect(page.locator('#report')).toHaveText('Generated', { timeout: 30_000 });

What replaces document.getElementById?

The closest Playwright equivalent to document.getElementById('save-button') is:

const saveButton = page.locator('#save-button');

Unlike a direct browser DOM lookup, this returns a Locator that can re-resolve the element and wait during an action or assertion. You can evaluate DOM properties when necessary, but keep normal interactions in Playwright’s locator API:

const disabled = await page.locator('#save-button').isDisabled();
const value = await page.locator('#email').inputValue();

Use page.evaluate() for application-specific browser-side computation, not as a substitute for every locator action. Direct DOM handles can become stale after navigation or framework re-rendering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting ID selectors

“Locator resolved to 0 elements”

  • Confirm the exact spelling and capitalization of the ID in the rendered DOM.
  • Check that you navigated to the expected URL and that the element is not inside an iframe.
  • Wait for the application state that creates the element rather than adding a fixed delay.
  • Inspect whether a cookie or authentication flow redirected the page before the element appeared.
console.log(await page.locator('#save-button').count());

A count of zero at the moment you check means the current document has no matching element; it does not prove that the element will never appear.

“Strict mode violation”

The selector matched more than one element for an operation that requires one. Correct duplicate IDs, scope the locator to a stable container, or use a role/name locator that uniquely identifies the intended control.

“Element is not visible” or “not receiving pointer events”

The node exists but is hidden, covered, outside an actionable state, or replaced during a transition. Assert visibility, wait for the relevant panel to open, and inspect overlays. Avoid force: true unless bypassing actionability is the behavior you deliberately want to test; it can make a test click an element a real user could not.

getByTestId() cannot find an element with an ID

Verify the markup. id="save-button" is not data-testid="save-button". Use page.locator('#save-button'), add a test-ID attribute, or configure the project’s test-ID attribute intentionally.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

The selector breaks after a redesign

If the ID was an implementation detail, move to a role, label, or visible-text locator that expresses the user-facing behavior. If the test needs a durable automation contract, add a stable data-testid and use getByTestId(). Avoid replacing one brittle selector with a longer CSS chain.

Performance and maintainability

Finding a unique ID is normally inexpensive, but test-suite reliability depends more on selector stability and waiting behavior than on micro-optimizing selector syntax. Define locators close to the action, or expose them through a page-object method when the same control is used across many tests:

export class SettingsPage {
  constructor(page) {
    this.page = page;
    this.saveButton = page.locator('#save-button');
  }

  async save() {
    await this.saveButton.click();
  }
}

Keep the page object’s locator lazy; do not resolve it once in a way that survives navigation as a stale handle. Prefer one unique, readable selector over a chain of layout-dependent selectors. When an ID is intentionally the contract, document that contract in the component or test fixture so a future refactor does not silently change its meaning.

Or skip the browser setup

If your goal is to obtain a clean screenshot rather than interact with the element in a Playwright test, ScreenshotNeo can capture the page through one request. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots; the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

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

Use the API documentation at screenshotneo.com/docs/ for authentication and options. A minimal cURL request is:

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

The same request in 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)

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

ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets and custom viewports, retina scale, PDF page settings, HTML/CSS rendering, custom JavaScript and CSS, pre-capture clicks, selector or network-idle waits, request and resource blocking, custom headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work, which can simplify migration.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to start.

Frequently Asked Questions

Should I use #id or id=value in Playwright?

Use #id for the concise, familiar CSS form. Use id=value when making Playwright’s ID selector engine explicit; both select the same HTML ID.

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.

Can I use getByTestId() for an HTML id?

Not by default. getByTestId() searches data-testid or the attribute configured as the project’s test ID. Use locator('#your-id') for an HTML ID.

Why does an ID locator fail inside an iframe?

A page locator searches the main document. Create a frameLocator() for the iframe first, then call locator('#id') on that frame locator.

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.