Playwright Locator Best Practices (2026) — The Only Guide You Need
Master Playwright locators the right way: getByRole vs getByTestId vs CSS vs XPath, live-tested examples, Page Object patterns, and how to kill flaky tests. Includes a free XPath & CSS selector generator.

Last updated: July 17, 2026 · 11 min read · By Avinash Kamble, reviewed by Priyanka G.
Every senior QA engineer eventually reaches the same conclusion: locators are the number-one predictor of a stable test suite. Get them right and your Playwright pipeline runs green for weeks. Get them wrong and you spend Friday afternoons rewriting selectors after a designer moves a div. This guide is the distilled 2026 playbook — the exact strategy we use across our own QA Practice Hub and recommend in the Playwright interview prep.
Key takeaways
- Prefer
getByRoleandgetByTestId— the Playwright team, MDN and W3C ARIA all agree.- Every absolute XPath in your suite is a future flaky test. Refactor them now.
- Generate and validate locators against real HTML using our free XPath & CSS Selector Generator.
- Wrap locators in a Page Object; never let raw selectors leak into test bodies.
- Assert with
expect(locator).toBeVisible(), notwaitForTimeout.
1. Why locator strategy is the #1 driver of flaky tests
Analyse any 3-month flaky-test report from a large Playwright suite and 70–80% of the top offenders share the same root cause: a locator that matched the DOM last sprint but doesn't match today. Class names get renamed by Tailwind refactors, wrappers get inserted by design-system upgrades, ids get hashed by build tools, and text copy gets tweaked by product managers. Every one of those events breaks a fragile selector.
The good news: Playwright ships with a hierarchy of user-facing locators that are essentially immune to visual refactors because they mirror how a real user (or a screen reader) perceives the page. Use them and your suite becomes robust by default.
2. The Playwright locator hierarchy — best to worst
+----------------------------------------------------------------------+
| PLAYWRIGHT LOCATOR HIERARCHY (STABLE → FRAGILE) |
+----------------------------------------------------------------------+
| 1. page.getByRole('button', { name: 'Sign in' }) ⭐ RECOMMENDED |
| 2. page.getByTestId('submit-btn') ⭐ RECOMMENDED |
| 3. page.getByLabel('Email') ✅ good |
| 4. page.getByPlaceholder('you@example.com') ✅ good |
| 5. page.getByText('Forgot password?') ✅ good |
| 6. page.locator('#submit-btn') ⚠️ ok if stable |
| 7. page.locator('form.auth input[name=email]') ⚠️ fragile |
| 8. page.locator('xpath=/html/body/div[2]/form/..') ❌ never ship it |
+----------------------------------------------------------------------+The official Playwright docs explicitly recommend the top of this list. Everything below getByText should be treated as a warning sign in code review — acceptable only when nothing more stable exists.
3. getByRole — the accessibility-first default
getByRole queries the accessibility tree, not the DOM. That means it matches the element the way a screen reader would — by its ARIA role (button, link, textbox, heading, dialog…) and its accessible name (visible text, aria-label, associated label). Two consequences:
- Your tests double as an accessibility audit. If
getByRole('button', { name: 'Submit' })can't find your button, neither can a screen reader user. - Refactors that don't change semantics don't break tests. Swap a
<div onclick>for a<button>? The role stays 'button' — test still passes.
// ✅ Accessibility-first, stable across visual refactors
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('you@example.com');
await expect(page.getByRole('heading', { name: 'Welcome back' })).toBeVisible();Common gotcha: implicit roles. <a href> is 'link', <input type="checkbox"> is 'checkbox', <h1>–<h6> are 'heading'. Use the W3C HTML-ARIA mapping when unsure.
4. getByTestId — the pragmatic escape hatch
Not every element has a meaningful role. A pricing tier card, a hidden feature flag toggle, a specific row in a data grid — these are best selected with an explicit data-testid that developers commit to preserving. Once a data-testid ships, changing it becomes a breaking change that shows up in code review — exactly the contract you want.
// Playwright — clean and stable
await page.getByTestId('pricing-tier-pro').click();
// In the HTML — developers own this attribute
// <div data-testid="pricing-tier-pro" class="px-4 py-6 rounded-2xl">...</div>Configure Playwright's test-id attribute globally so you're not tied to data-testid specifically:
// playwright.config.ts
export default defineConfig({
use: { testIdAttribute: 'data-qa' },
});Team rule of thumb: add a data-testid the moment you write a test whose only stable option is a fragile CSS selector. Push it as part of the same PR as the test.
5. CSS vs XPath — when to use each
When you must fall back to a raw selector, prefer CSS attribute selectors over XPath. CSS is faster, better supported by browsers, and easier for future maintainers to parse. Use XPath only when you specifically need capabilities CSS doesn't have — text matching, axis traversal (ancestor::, following-sibling::), or attribute predicates with logic.
// ✅ CSS — first choice for attribute-based selection
page.locator('input[name="email"][type="email"]');
page.locator('button[data-status="loading"]');
// ✅ XPath — legitimate use cases
page.locator('xpath=//tr[td[normalize-space()="Acme Corp"]]/td[3]/button');
page.locator('xpath=//label[contains(text(), "Terms")]/preceding-sibling::input');
// ❌ Absolute XPath — breaks on any DOM change
page.locator('xpath=/html/body/div[2]/div[1]/form/div[3]/button');Every absolute XPath (one that starts /html/) in your suite is a Friday afternoon waiting to happen. Refactor them proactively. Not sure what to replace them with? Paste the surrounding HTML into our free XPath & CSS Selector Generator — it validates each candidate against the DOM and flags fragile ones automatically.
6. Chaining and filtering — scoping locators the safe way
Complex UIs have repeated patterns — twenty rows in a table, six pricing cards on a marketing page. Instead of writing an elaborate single selector, chain locators to scope the search:
// Scope: find the 'Delete' button inside the row whose first cell contains 'Acme'
const row = page.getByRole('row').filter({ hasText: 'Acme' });
await row.getByRole('button', { name: 'Delete' }).click();
// Scope a dialog and interact only within it
const dialog = page.getByRole('dialog', { name: 'Confirm delete' });
await dialog.getByRole('button', { name: 'Yes, delete' }).click();Chained locators read like English, survive DOM changes, and give better error messages when they fail (Playwright reports the exact step in the chain that didn't match).
7. Auto-waiting assertions — never write waitForTimeout
The single biggest source of flakiness after bad locators is hardcoded waits. Playwright's expect(locator).toBe*() assertions retry automatically until the condition is met or a timeout expires. Use them everywhere:
// ❌ Flaky and slow
await page.click('#submit');
await page.waitForTimeout(3000);
const text = await page.locator('.status').textContent();
expect(text).toBe('Saved');
// ✅ Auto-waits, fast when the app is fast
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');Available: toBeVisible, toBeEnabled, toBeChecked, toHaveText, toHaveValue, toHaveCount, toHaveURL, toHaveAttribute. If you're still typing waitForTimeout, you're leaving reliability on the table.
8. Encapsulate locators in a Page Object
Never let a raw selector leak into a test body. Wrap every locator in a Page Object class — this is the single change that most improves a suite's long-term maintenance cost. When the DOM changes, you update one file, not fifty tests.
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
constructor(private readonly page: Page) {}
readonly email = this.page.getByRole('textbox', { name: 'Email' });
readonly password = this.page.getByRole('textbox', { name: 'Password' });
readonly submit = this.page.getByRole('button', { name: 'Sign in' });
readonly error = this.page.getByRole('alert');
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
}
}Don't want to hand-write POM classes? Our selector generator emits a full Page Object class for Playwright TypeScript, Selenium Java, or Cypress JavaScript in one click — paste the HTML, click Copy, commit.
9. iframes, Shadow DOM and other edge cases
Modern apps embed content — payment widgets, video players, embedded design tools. Two rules cover most of them:
- iframes: use
page.frameLocator('iframe[title="Stripe"]').getByRole('textbox', { name: 'Card number' }). Never try to reach into an iframe with a normal locator — it won't work. - Shadow DOM: Playwright's locators pierce open shadow roots automatically.
getByRoleworks across shadow boundaries. For closed shadow roots, there is no supported access — that's the platform's contract.
Web Components with a lot of shadow DOM are where getByRole and getByTestId earn their keep — CSS selectors simply can't traverse shadow trees reliably.
10. The 60-second locator code-review checklist
Print this list. Paste it into your PR template. Reject any test PR that fails it:
- Does the locator use
getByRole,getByTestId,getByLabel, orgetByPlaceholder? If yes → approve. - Is there a raw
xpath=/html/...? Reject — request refactor. - Is a raw class selector used (
.btn-primary-xl)? Ask if a role or test-id would work. - Is the locator scoped with
.filter()or a parent chain? If iterating over similar elements, it should be. - Are there any
waitForTimeout()calls? Replace withexpect(locator).toBe*(). - Do reused locators live in a Page Object or inline in the test? Move them out.
Ship this discipline for one quarter and your flake rate typically drops by 60–80%.
FAQ — Playwright locator questions engineers actually ask
Q: When should I still use CSS selectors over getByRole?
When the element has no meaningful ARIA role and no data-testid — for example, a decorative wrapper you want to assert exists. Even then, add a data-testid instead if you own the codebase.
Q: Is getByText fragile because product managers rewrite copy?
Yes, in fast-moving marketing UIs. In stable interactive UIs (buttons, links, headings) it's fine. Use getByRole with the name option when you can — it matches accessible name, which is more forgiving than exact text.
Q: Should I ban XPath entirely?
No. XPath's text and axis capabilities are legitimately useful (finding the button inside the row that contains a specific customer name, for example). Ban absolute XPath. Allow relative XPath with review.
Q: Do these rules apply to Selenium and Cypress?
Yes — the principles transfer directly. Selenium 4's relative locators and Cypress's cy.findByRole() (via Testing Library) map to the same hierarchy. See our Selenium interview prep and Cypress vs Playwright 2026 comparison.
Next steps
Ready to put this into practice?
- 🛠️ Generate locators from real HTML with our free XPath & CSS Selector Generator — includes a Page Object exporter and bulk CSV export.
- 📚 Deepen your Playwright knowledge with the Playwright interview questions hub.
- 🔬 Practise on real DOM patterns in the QA Practice Hub.
- 📈 Compare frameworks in Cypress vs Playwright 2026.
Practice these questions
Drill 200+ Playwright questions with senior-SDET sample answers — locators, auto-wait, fixtures, parallelism and trace viewer.
Was this article helpful?
More from Playwright TypeScript
Playwright with TypeScript — POM, fixtures, locators.
- Automation TestingPlaywright Framework Setup with TypeScript: Complete 2026 Guide for QA Engineers
- Automation TestingPlaywright TypeScript Tutorial: Complete 2026 Guide
- Automation TestingPlaywright Locators: Complete 2026 Guide
Keep building your QA edge
Pillar guides- Selenium PillarSelenium WebDriver guide300 Selenium WebDriver Q&A — locators, waits, frameworks.
- Playwright Installation Guidehow to install Playwright step by stepInstall Playwright the right way — Node, browsers, VS Code, first test.
- XPath & CSS Selector Generatorgenerate robust Playwright and Selenium locatorsInteractive locator generator for Playwright, Selenium and Cypress — with Page Object export.
Practice these questions live
Rehearse with an AI QA interviewer that scores your answers in real time.
Continue reading

Why Every QA Engineer Must Master CI/CD Pipelines in 2026 (Or Risk Obsolescence)
12 min read
Is Cypress Dead? Analyzing 2026 Playwright Market Share
12 min read
Why Tests Pass Locally But Fail in CI/CD (And the 6 Fixes That Actually Work in 2026)
13 min readJoin the QA Community
Connect with fellow testers, share job leads, and get career advice.
Stop Reinventing the Wheel. Upgrade Your QA Arsenal.
Take your testing skills from beginner to Lead Engineer. Supercharge your daily workflow with our premium digital resources.
- Ready-to-use testing strategy templates
- Advanced API & UI automation guides
- ⏱️ Save 10+ hours a week on test planning