ScreenshotNeo

BlogGuides

Playwright Locators: How to Find Elements Reliably

Choose Playwright locators that match user-facing meaning, make repeated elements unique, and fix strict mode and timeout errors without brittle selectors.

By the ScreenshotNeo team4 October 20268 min read

To find an element reliably in Playwright, start with what a user or assistive technology can identify: use getByRole() with an accessible name for controls, or getByLabel() for labeled form fields. Scope repeated elements to a meaningful card, row, or dialog, then make sure the locator matches exactly the intended target. Use a test ID when an explicit internal test contract is what you need; use CSS or XPath when structure itself is the contract. Playwright’s locators resolve against the current page when used, and its auto-waiting helps with readiness. Neither feature can make a semantically wrong locator correct.

1. Choose a locator that reflects the test contract

Ask what the test needs to protect: an accessible role and name, visible copy, a deliberate test hook, or a structural detail. Choose the locator that expresses that requirement directly.

Target or intent Preferred locator Why
Button, link, checkbox, or other semantic control getByRole(role, { name }) Targets the role and accessible name exposed to users and assistive technology.
Form field with an associated label getByLabel() Targets the field through its label.
Visible non-interactive copy getByText() Targets text that is part of the user-visible content contract.
Field with a meaningful placeholder but no label getByPlaceholder() Can target the placeholder; a placeholder is not a replacement for a proper accessible label.
Image or titled element getByAltText() or getByTitle() Uses the attribute that is intended to identify the target.
Deliberate internal test hook getByTestId() Uses an explicit testing contract, independent of user-facing wording.
Structural detail is itself under test, or no suitable built-in fits locator() with CSS or XPath Can describe structure, but may couple the test to implementation details.

Playwright describes locators as central to its auto-waiting and retry behavior. Its locator guide recommends role locators because they align with how users and assistive technology perceive a page. The built-in locator methods include getByRole(), getByLabel(), getByText(), getByPlaceholder(), getByAltText(), getByTitle(), and getByTestId(). See the official Playwright locator documentation.

2. Runnable setup and basic locator examples

Install Playwright Test in a Node.js project with npm init playwright@latest, then place a test in the generated test directory. This example assumes the application is available at http://localhost:3000. Adjust the URL and expected page content for your app.

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

test('user can save profile details', async ({ page }) => {
  await page.goto('http://localhost:3000/profile');

  await page.getByLabel('Email').fill('reader@example.com');
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.getByText('Profile saved')).toBeVisible();
});

For buttons and links, include the accessible name whenever it distinguishes the target. The name option can also accept a regular expression. By default, text matching normalizes whitespace; use exact: true when an exact string match is the intended contract.

// Semantic role and accessible name
await page.getByRole('button', { name: 'Save changes' }).click();

// Exact visible text
await expect(page.getByText('Payment complete', { exact: true })).toBeVisible();

// Regular-expression name when the wording has an intentional variable part
await page.getByRole('button', { name: /save/i }).click();

// Placeholder targeting is useful when that is the available contract
await page.getByPlaceholder('Search products').fill('notebook');

// Image alternative text or title attribute
await page.getByAltText('Blue running shoes').click();
await page.getByTitle('Close dialog').click();

Use a test ID when a stable internal hook is explicitly the test’s contract. It does not verify that user-facing text or semantics are correct. Conversely, a role/name locator will catch a regression in the accessible role or name when that property is part of the behavior under test.

// Example markup: <button data-testid="checkout-submit">Continue</button>
await page.getByTestId('checkout-submit').click();

3. Make repeated elements unique by scoping

A locator can describe many matching elements. Actions such as click() expect one target and fail in strict mode if multiple elements match. Add a meaningful name, or first identify a containing item such as a product card, list item, table row, or dialog. Then locate the intended descendant within it.

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

test('add the intended product to cart', async ({ page }) => {
  await page.goto('http://localhost:3000/products');

  const product = page
    .getByRole('listitem')
    .filter({ has: page.getByRole('heading', { name: 'Product 2' }) });

  await expect(product).toHaveCount(1);
  await product.getByRole('button', { name: 'Add to cart' }).click();
});

Filters such as hasText and has narrow the outer locator. The inner locator in has is evaluated relative to each candidate outer element, so keep it focused on a descendant that identifies the item. You can assert uniqueness with toHaveCount(1) when exactly one match is an intended invariant.

For dialogs, rows, and repeated cards, prefer stable semantic boundaries and meaningful content over an arbitrary ancestor selector. If the supposed boundary itself matches several elements, refine that boundary first.

4. When CSS, XPath, or positional locators make sense

page.locator() accepts CSS and XPath selectors. They are appropriate when there is no suitable user-facing locator, when an explicit test hook is unavailable, or when DOM structure is what the test deliberately covers. Keep selectors short and tied to an intentional contract.

// CSS by a deliberate application test hook
await page.locator('[data-testid="checkout-submit"]').click();

// CSS for a structural behavior that the test specifically covers
await page.locator('form.checkout input[name="postalCode"]').fill('10001');

// XPath is supported when the structural relationship is the contract
await page.locator('//button[@type="submit"]').click();

Long chains based on incidental classes, nesting, or layout are vulnerable to ordinary redesigns. Prefer a role/name or a deliberate test ID if those express the intended target more clearly.

first(), last(), and nth(index) select by position. Since reordering can silently change what the test operates on, use them only when ordering is itself the contract or no better discriminator exists.

// Appropriate only if the first matching option is intentionally the one to choose
await page.getByRole('option').nth(0).click();

5. Understand locator resolution and auto-waiting

A locator is a query, not a stored element handle. Playwright resolves it when an operation uses it, so a later operation can find the matching element again after the DOM changes. This supports retries and dynamic pages.

For a click, Playwright waits for the target to resolve uniquely and to be visible, stable, enabled, and able to receive events. If those checks do not pass before the timeout, the action fails. Auto-waiting handles transient readiness; it does not fix a locator that points to the wrong element or matches several elements. See the official actionability documentation.

Prefer web-first assertions such as toBeVisible(), toHaveText(), and toHaveCount(). They retry while the expected condition becomes true. Avoid replacing a missing condition with a fixed sleep before every action: a sleep can waste time when the page is ready early and still fail when it is slower than expected.

6. Diagnose common locator failures

Symptom Likely cause Fix
Strict mode violation A single-target action found multiple matches. Add the accessible name, scope to a meaningful card/row/dialog, or filter by distinguishing text or a child locator. Assert count one if uniqueness is part of the contract.
Action timed out The target did not become unique, visible, stable, enabled, or able to receive events in time; the page may also not have reached the expected state. Check the locator and page state first. Inspect whether an overlay blocks the target. Increase timeout only when the operation legitimately needs more time and the locator and state are correct.
Test breaks after a redesign The locator depends on incidental classes, deep nesting, or a DOM path that changed. Use a role/name or other user-facing property, or add a deliberate test ID contract if the target is intentionally internal.
Test passes but misses an accessibility or copy regression A test ID remains unchanged even though user-facing role or wording changed. Use a role/name or text locator when that user-visible property is what the test should protect.
Text locator unexpectedly matches or misses whitespace Text matching normalizes whitespace, or the exactness requirement was not expressed. Use exact: true for an exact string contract; otherwise make the expected text less brittle if formatting is not relevant.
Click is intercepted An overlay, animation, or another element receives pointer events instead. Wait for the relevant UI state or close the overlay through the intended user flow; confirm the target is the one the user could interact with.

7. Reliability, performance, and maintenance

  • Reliability: use a locator that expresses intent, make single-target operations unique, and wait on state with retrying assertions. A higher timeout does not improve selector correctness.
  • Performance: prefer focused locators and avoid repeatedly scanning broad sets when a meaningful parent can narrow the search. No locator style guarantees a measured speed advantage across applications; the page and query determine behavior.
  • Maintenance: role/name locators track the user-facing contract. Test IDs track an explicit internal contract. CSS/XPath track structure. Choose based on what a test should fail when that property changes.
  • Cost: Playwright locator methods have no per-call service charge. The practical cost is test runtime and maintenance; unnecessary fixed waits and broad retries can increase both.

8. Capture a page when debugging a locator

When a locator fails, a screenshot can help you inspect overlays, unexpected page state, or which repeated item appears on screen. Playwright can capture a screenshot directly:

await page.screenshot({ path: 'debug-page.png', fullPage: true });

For teams that want a separate capture endpoint, ScreenshotNeo is a website screenshot API and MCP server. The [API documentation] includes request options and examples.

9. Or skip the browser setup

For a one-off page capture, ScreenshotNeo takes a URL in one GET request and returns an image or PDF. Here is the cURL form:

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

Equivalent Python and Node.js examples are available in the ScreenshotNeo API docs:

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}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page info, and PDF capture. 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.

10. FAQ

Should I always use getByRole()?

Use it when the target’s semantic role and accessible name are relevant and available. For other contracts, use the locator that states the intended property.

Are test IDs bad practice?

No. They are useful when an explicit internal test hook is the contract. They simply do not test the user-facing name or role.

Can I use a regular expression for a locator name?

Yes. Role and text locator options support regular expressions, which can express intentional variable wording. Keep the expression specific enough to identify the intended target.

Does auto-waiting mean I never need to wait?

No. Actions wait for their actionability conditions, and assertions retry for their expected condition. For a distinct application state, wait for that state explicitly with a locator or assertion.

Official references