PRACTICAL GUIDE / playwright locators guide
Playwright Locators Guide: Find Elements Reliably
Playwright locators guide for stable UI tests with role selectors, filters, assertions, strict mode, debugging, and flaky selector fixes in CI.
In this guide9 sections
- Choose the Contract Before the Syntax
- Start With Role and Accessible Name
- Narrow Repeated UI With Meaningful Context
- Let Strictness Expose Ambiguity
- Understand Why Locators Wait Reliably
- Use CSS, XPath, and Test IDs With Intent
- Locate Frames, Dialogs, and Shadow Content Correctly
- Debug the Match Before Editing It
- Adopt a Locator Review Order
What you will learn
- Choose the Contract Before the Syntax
- Start With Role and Accessible Name
- Narrow Repeated UI With Meaningful Context
- Let Strictness Expose Ambiguity
Flaky selectors are rarely fixed by making them longer. A path such as div:nth-child(3) > button can identify one element today while encoding nothing about why that button matters. When the layout changes, the test fails even if the user's task still works.
Playwright locators are designed around live queries, automatic waiting, and strict targeting. The durable approach is to locate a control by the contract a user or the product recognizes, then narrow it with meaningful context.
This also changes code review. A reviewer should be able to explain the selected element from the locator alone. If understanding it requires opening the DOM and counting wrappers, the selector has already coupled the test to an implementation detail.
Choose the Contract Before the Syntax
A locator is an agreement between the test and the interface. Decide which interface property should remain stable when markup and layout change.
| Product contract | Playwright locator | Good use |
|---|---|---|
| Accessible role and name | getByRole() | Buttons, links, headings, controls |
| Form label | getByLabel() | Inputs associated with labels |
| Placeholder | getByPlaceholder() | When placeholder is meaningful and stable |
| Visible copy | getByText() | Messages and content whose wording matters |
| Image alternative text | getByAltText() | Images with meaningful alt text |
| Stable test attribute | getByTestId() | Complex or repeated components |
| Stable DOM attribute | locator() with CSS | Technical surfaces without accessible contract |
Role and label locators do more than reduce selector churn. They reveal controls that are inaccessible or ambiguously named. A button implemented as a clickable div may not appear as a button to assistive technology, and a role-based test makes that problem visible.
Use test IDs when user-facing attributes are intentionally unstable or insufficient. A test ID is better than a fragile hierarchy, but it should identify product purpose, such as checkout-total, not implementation, such as blue-text-4.
Start With Role and Accessible Name
For interactive elements, role plus accessible name is usually the clearest target:
import { test, expect } from '@playwright/test'
test('buyer submits an order', async ({ page }) => {
await page.goto('/checkout')
await page.getByLabel('Email address').fill('buyer@example.com')
await page.getByRole('button', { name: 'Place order' }).click()
await expect(
page.getByRole('heading', { name: 'Order confirmed' }),
).toBeVisible()
})The accessible name may come from text, an associated label, aria-label, or another accessibility relationship. Inspect what the browser exposes rather than assuming the visible string is the name.
Use exact matching when similar names are valid:
await page.getByRole('button', { name: 'Save', exact: true }).click()Without exact, a control named “Save as draft” may also match depending on the name comparison. Exactness is not automatically better. Choose it when the product distinguishes those actions.
Narrow Repeated UI With Meaningful Context
Lists and cards legitimately repeat buttons. Do not solve that by taking the first or third match. Locate the container that represents the desired record, then locate within it.
const product = page
.getByRole('article')
.filter({ has: page.getByRole('heading', { name: 'Wireless Mouse' }) })
await product.getByRole('button', { name: 'Add to cart' }).click()The test now says which product it selects. If sorting changes, the intent remains intact.
Text filters can narrow a row:
const order = page
.getByRole('row')
.filter({ hasText: 'ORD-1042' })
await expect(order.getByRole('cell', { name: 'Dispatched' })).toBeVisible()Keep the filtering locator relative to the container. Starting from page inside a has filter can accidentally search outside the intended component. When the relationship becomes difficult to express, ask whether the UI can expose a better role, accessible name, or test ID.
Let Strictness Expose Ambiguity
Actions such as click() require a locator that resolves to one element. If two “Delete” buttons match, Playwright reports a strict mode violation rather than guessing. That failure is useful. It tells you the test has not stated which record to delete.
Avoid reflexively fixing it with .first(), .last(), or .nth(2). Those methods are appropriate when position is the behavior, such as verifying the first item after sorting. They are fragile when position is only a shortcut.
Count before acting when multiplicity is itself the assertion:
const errors = page.getByRole('alert')
await expect(errors).toHaveCount(2)
await expect(errors).toContainText([
'Email is required',
'Password is required',
])Do not turn off the signal by constructing a broad locator and selecting an arbitrary match. Refine it with a named region, row, dialog, form, or other semantic container.
Understand Why Locators Wait Reliably
A locator describes how to find an element when an action or assertion runs. It does not freeze the first DOM node it sees. If React re-renders a button between calls, the locator resolves again.
Before a click, Playwright checks conditions such as visibility, stability, event reception, and enabled state. Assertions like toBeVisible() retry until the expected condition or timeout. This is why ordinary Playwright code should not need sleeps.
await page.getByRole('button', { name: 'Refresh status' }).click()
await expect(page.getByTestId('order-status')).toHaveText('Ready')The assertion waits for “Ready.” Adding waitForTimeout(2000) would make fast runs slower and still fail when processing takes longer.
Automatic waiting does not prove the application reached the right business state. A button can be visible while stale data remains on screen. Wait or assert on the state transition the user cares about, such as a status, URL, response-driven result, or removed loading indicator.
Be cautious with force: true. It bypasses some actionability checks and can hide overlays or disabled interactions that block real users. Use it only when the test intentionally exercises behavior that cannot be performed like a user.
Use CSS, XPath, and Test IDs With Intent
CSS locators remain useful for structural or technical elements without a suitable accessible contract:
const invalidEmail = page.locator('input[name="email"]:invalid')
await expect(invalidEmail).toBeVisible()This selector expresses a browser validation state. It is more meaningful than a complete ancestry path.
XPath can traverse relationships CSS cannot express in an older application, but it often becomes a copied DOM blueprint. Keep any XPath short, document why semantic locators were unavailable, and treat repeated XPath as testability debt.
Configure a project-specific test ID attribute if the application already has a convention:
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
use: {
testIdAttribute: 'data-qa',
},
})Then page.getByTestId('payment-summary') resolves data-qa="payment-summary". Keep IDs unique within the relevant page or component and do not generate them from array indexes.
Locate Frames, Dialogs, and Shadow Content Correctly
An iframe has its own document. Use a frame locator rather than searching the top page:
const payment = page.frameLocator('iframe[title="Secure payment"]')
await payment.getByLabel('Card number').fill('4111111111111111')Confirm that automating real payment-provider fields is allowed in the test environment. Often the safer contract is a provider sandbox and known test values.
For an HTML dialog, scope to the dialog role:
const dialog = page.getByRole('dialog', { name: 'Delete order' })
await dialog.getByRole('button', { name: 'Cancel' }).click()Browser-native JavaScript dialogs are events, not DOM locators. Handle them through the page's dialog event. Playwright locators pierce open shadow DOM by default for supported selector strategies, but XPath does not pierce shadow roots. Closed shadow roots require a product-level testing seam or different coverage.
Debug the Match Before Editing It
When a locator fails, establish whether it matched zero, one, or many elements and whether the target was actionable. Use the inspector, trace viewer, UI mode, and locator assertions. A temporary count is useful during diagnosis:
const submit = page.getByRole('button', { name: 'Submit' })
console.log('submit matches:', await submit.count())Then investigate the category:
| Failure | Question to answer |
|---|---|
| Zero matches | Is the page, frame, role, name, or state wrong? |
| Multiple matches | Which user-recognizable container disambiguates it? |
| Not visible | Is it hidden, offscreen, collapsed, or duplicated? |
| Not enabled | What state enables it, and did setup reach that state? |
| Events intercepted | Is a modal, toast, spinner, or overlay covering it? |
| Works locally only | Do locale, data, feature flags, or viewport differ? |
Code generation can suggest a starting locator, but review the result. Generated selectors know the current page, not which attributes your product promises to keep stable.
Adopt a Locator Review Order
For each new interaction, try role and accessible name, then label or meaningful text, then a stable test ID, and finally a concise CSS or justified XPath. Scope repeated controls to a semantic container. Treat strictness errors as missing intent, not framework inconvenience.
As a concrete next step, search the suite for .nth(, long CSS chains, and XPath. Pick the ten selectors tied to the most frequently changed screens. Replace each with a user-facing or purpose-based contract, then run those specs in two locales or data states. That small review will reveal both fragile tests and accessibility gaps without demanding a full framework rewrite.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
What is the best locator in Playwright?
The best locator is usually a user facing locator such as getByRole, getByLabel, getByText, or getByTestId when the product exposes stable test ids. Prefer locators that match how users perceive the page, then use filters for precision.
Should I use XPath in Playwright?
Use XPath only when no user facing or stable attribute based locator is available. Playwright supports XPath, but role, label, text, test id, and CSS locators are easier to read, debug, and maintain in most application test suites.
Why are Playwright locators strict?
Playwright locators are strict so a click or assertion targets exactly one element. If a locator matches several elements, Playwright asks you to refine it. This prevents accidental clicks on the wrong button and catches ambiguous page structure early.
How do I debug a Playwright locator?
Use the Playwright inspector, codegen, trace viewer, locator.highlight, and expect assertions to see what a locator matches. Check whether the element is visible, enabled, unique, attached, and described by accessible role or label.
Are test ids bad in Playwright?
Test ids are not bad. They are useful for complex components, repeated layouts, and text that changes often. Do not use them as a first escape hatch for every element, but agree on stable test ids for controls that lack accessible names.
RELATED GUIDES
Continue the learning route
GUIDE 01
Playwright Tutorial: End-to-End Testing from Scratch
Playwright tutorial for beginners: install, write your first test, TypeScript setup, codegen, fixtures, API testing, debugging, and CI tips in one guide.
GUIDE 02
CSS Selectors vs XPath: A Cheat Sheet for Testers
Compare CSS selectors vs XPath for test automation, with a cheat sheet, speed notes, Playwright locator advice, and stable data-testid practices.
GUIDE 03
Page Object Model: A Pattern for Maintainable Tests
Learn the page object model for maintainable UI automation tests, with Playwright examples, best practices, and common POM mistakes to avoid.
GUIDE 04
Selenium vs Playwright vs Cypress in 2026
Compare Selenium vs Playwright vs Cypress in 2026 for speed, browsers, CI, flakiness, ecosystem, and which web automation framework beginners should choose.