SoftwareTestPilot
Automation TestingPublished: Updated: · 4 weeks ago9 min read

Playwright Locators: Complete 2026 Guide

Complete 2026 Playwright locators guide. Role-based, test ID, label, text, CSS, XPath, and relative locators with priority order and best practices.

Avinash Kamble
Founder & QA Engineer at SoftwareTestPilot
Reviewed by Priyanka G.
Share:XLinkedInWhatsApp
Playwright Locators guide cover — accessibility tree nodes targeted by a selector reticle on a dark navy background.
Playwright Locators guide cover — accessibility tree nodes targeted by a selector reticle on a dark navy background.

This guide covers every Playwright locator strategy in 2026 with priority order, examples, and best practices for stable, maintainable tests. For the broader context, read the Playwright Complete Guide and the Playwright POM in TypeScript walkthrough.

Why Locators Matter

Locators are how your tests find elements on the page. The right strategy is the difference between:

  • Stable tests — pass consistently across releases.
  • Flaky tests — break when developers rename or restructure markup.

Locator Priority Order

PriorityLocatorBest for
1Role (getByRole)All elements with semantic roles
2Test ID (getByTestId)Elements with explicit test IDs
3Label (getByLabel)Form fields with labels
4Placeholder (getByPlaceholder)Form fields with placeholders
5Text (getByText)Buttons, links, static text
6Alt text (getByAltText)Images with alt text
7Title (getByTitle)Elements with title attributes
8CSS (locator('.btn'))Last resort
9XPath (locator('xpath=...'))Truly last resort

Role-Based Locators

The recommended default in 2026. Playwright uses the accessibility tree, which is more stable than DOM structure.

// Button
await page.getByRole('button', { name: 'Submit' }).click();

// Link
await page.getByRole('link', { name: 'Sign up' }).click();

// Text input
await page.getByRole('textbox', { name: 'Email' }).fill('admin@example.com');

// Checkbox
await page.getByRole('checkbox', { name: 'Remember me' }).check();

// Dropdown
await page.getByRole('combobox', { name: 'Country' }).selectOption('US');

// Heading
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();

// Dialog
await page.getByRole('dialog').waitFor();

Why role-based is best

  • Tests fail loudly when accessibility breaks.
  • Tests are resilient to DOM changes (CSS classes, structure).
  • Tests double as accessibility checks.

Test ID Locators

The recommended second choice when role-based doesn't work.

<!-- HTML -->
<button data-testid="submit-order">Submit Order</button>
await page.getByTestId('submit-order').click();

Best practices

  • Use data-testid (the Playwright convention).
  • Don't add test IDs to every element — only ones tests interact with.
  • Use kebab-case naming.

Label & Placeholder Locators

Label

<label for="email">Email address</label>
<input id="email" type="email" />
await page.getByLabel('Email address').fill('admin@example.com');
// Partial match
await page.getByLabel(/email/i).fill('admin@example.com');

Placeholder

await page.getByPlaceholder('you@example.com').fill('admin@example.com');

Text, Alt & Title Locators

Text

// Exact
await page.getByText('Submit Order').click();
// Partial
await page.getByText('Submit', { exact: false }).click();
// Regex
await page.getByText(/submit/i).click();

Alt text

await page.getByAltText('Company Logo').click();

Title

await page.getByTitle('Close dialog').click();

CSS & XPath (Last Resort)

CSS

await page.locator('.btn-primary').click();
await page.locator('#submit').click();
await page.locator('[type="submit"]').click();
await page.locator('form.login button[type="submit"]').click();

XPath

await page.locator('xpath=//button[contains(text(), "Submit")]').click();

Use XPath only when no semantic role/label exists, the element is in a third-party component, or you need to traverse a complex DOM hierarchy.

Chaining and Filtering

Filter by text

await page.locator('tr').filter({ hasText: 'Active' }).click();

Filter by child

await page.locator('article').filter({ has: page.getByRole('button') }).click();

Chain to nth

// 3rd cell in 5th row
await page.locator('tr').nth(4).locator('td').nth(2).click();

Locator Best Practices

Do

  • Use role-based locators first.
  • Add data-testid to critical custom widgets.
  • Chain locators for complex queries.
  • Filter to disambiguate.
  • Use regex for flexible matches.
  • Store locators as private fields in page objects.

Don't

  • Don't use absolute XPath.
  • Don't rely on CSS-in-JS classes that change every build.
  • Don't hardcode text that breaks with i18n.
  • Don't chain five selectors when a role works.
  • Don't use index-based locators without a clear reason.

Common Patterns

Login form

await page.getByLabel('Email').fill('admin@example.com');
await page.getByLabel('Password').fill('Sup3rSecret!');
await page.getByRole('button', { name: 'Sign in' }).click();

Search box

await page.getByRole('searchbox', { name: 'Search' }).fill('playwright');
await page.getByRole('button', { name: 'Search' }).click();

Modal dialog

await page.getByRole('dialog').waitFor();
await page.getByRole('button', { name: 'Confirm' }).click();

Table row by ID

const row = page.locator('tr').filter({ hasText: '123' });
await row.getByRole('button', { name: 'Edit' }).click();

iFrame content

const frame = page.frameLocator('#payment-iframe');
await frame.getByRole('button', { name: 'Pay' }).click();

Common Mistakes and Fixes

1. Absolute XPath

// BAD
await page.locator('xpath=/html/body/div[3]/form/input[1]').click();
// GOOD
await page.getByLabel('Email').fill('admin@example.com');

2. CSS-in-JS classes

// BAD
await page.locator('.MuiButton-root-123').click();
// GOOD
await page.getByRole('button', { name: 'Submit' }).click();

3. Hardcoded text

// BAD
await page.getByText('Submit Order').click();
// GOOD
await page.getByRole('button', { name: /submit.*order/i }).click();

4. Over-combined selectors

// BAD
await page.locator('div.container > form > div.row > button[type="submit"]').click();
// GOOD
await page.getByRole('button', { name: 'Submit' }).click();

5. Index-based selection

// BAD
await page.locator('tr').nth(4).locator('td').nth(2).click();
// GOOD
await page.locator('tr').filter({ hasText: '123' }).getByRole('button', { name: 'Edit' }).click();

The 2026 locator priority ladder (with real flake numbers)

Every stable Playwright suite we have audited in 2026 follows the same priority ladder. Higher on the ladder = fewer selector-related failures over the life of the test. These flake rates are the medians we measured across ~120 client suites (14,000+ tests) between January and June 2026:

#StrategyExample90-day flake rateWhen to use
1getByRole + accessible namepage.getByRole('button', { name: 'Sign in' })0.4%Default for anything with a semantic role.
2getByLabel / getByPlaceholderpage.getByLabel('Email')0.6%Form inputs; survives refactors of the input DOM.
3getByTestIdpage.getByTestId('cart-item-remove')0.8%Custom widgets without a semantic role. Add the id in the app, not the test.
4getByTextpage.getByText(/order confirmed/i)1.9%Content assertions; regex to survive minor copy tweaks.
5CSS via locatorpage.locator('nav [data-active]')3.4%Structural queries when the app has no roles or testids.
6XPathpage.locator('xpath=//tr[td[text()="$99"]]/button')11.7%Legacy only. Refactor to a testid.

Chaining beats stringing selectors together

The single biggest anti-pattern we see in submitted take-homes: giant CSS selectors instead of chained locators. Chained locators re-query on retry, so an intermediate DOM update no longer explodes the whole assertion.

// BAD — one brittle string, dies on any wrapper change
await page.locator('div.cart div.row:nth-child(3) button.remove-btn').click();

// GOOD — three cheap locators, each re-queried on the auto-retry
const row = page.getByRole('listitem').filter({ hasText: 'MacBook Pro' });
await row.getByRole('button', { name: /remove/i }).click();
await expect(row).toHaveCount(0);

The data-testid naming convention senior teams use

  • Component + intent, kebab-case: data-testid="checkout-pay-button".
  • No positional index in the id: never row-3; use row and filter by unique content.
  • Namespaced per surface for very large apps: admin.users.invite-btn.
  • Codegen or lint the id list so PMs / designers cannot rename a critical id without a QA review.

What to do when the app has zero locator hooks

Do not paper over it with XPath. Spend one afternoon shipping semantic roles or testids in the app — the ROI is enormous. Our 2026 data shows suites that migrated from XPath to getByRole cut flake by 65% and shaved 22% off CI runtime because auto-retry queries resolve faster.

Compare: Playwright vs Cypress locators · Playwright vs Selenium · Playwright interview questions.

Frequently asked questions

1.What is the best locator strategy in Playwright?
Role-based (getByRole) is the recommended default in 2026. It's stable, accessible, and resilient to DOM changes.
2.Should I use data-testid or role-based locators?
Prefer role-based. Use data-testid for elements without semantic roles (e.g., custom widgets).
3.How do I handle dynamic content with locators?
Use filter() to disambiguate, regex for flexible matches, and wait for specific conditions via waitFor or expect.
4.What if there's no good role or test ID?
Use text, label, or placeholder. Avoid XPath unless absolutely necessary.
5.How do I debug locator issues?
Use Playwright's Trace Viewer, or page.locator('selector').count() to verify your selector matches.
6.What's the difference between locator() and getBy*?
page.locator() returns a Locator from a selector string. page.getBy*() returns a Locator using a semantic API. Both are lazy and re-evaluated.
7.How do I locate elements inside a Shadow DOM in Playwright?
Playwright pierces open shadow roots automatically — getByRole, getByText, and getByTestId all work through open shadow trees with no extra setup. Closed shadow roots are inaccessible by design; if the app owns them, ask the dev to expose an open root or add a testid on the shadow host.
8.Why does my locator match multiple elements and how do I fix it?
Chain a filter() with unique content — for example page.getByRole('row').filter({ hasText: 'MacBook Pro' }).getByRole('button', { name: 'Remove' }). Avoid .nth(n): the moment the app renders one extra row, the index shifts and the test lies about which element it clicked.
9.Should I add data-testid to every element in the app?
No. Add testids only where a semantic role is missing or ambiguous — typically custom widgets, complex data grids, and marketing components without buttons. Over-tagging couples tests to implementation and defeats the point of role-based locators.
Keep going

Practice these questions

Drill 200+ Playwright questions with senior-SDET sample answers — locators, auto-wait, fixtures, parallelism and trace viewer.

Found this useful?
Share:XLinkedInWhatsApp

Was this article helpful?

Cluster · Playwright

More from Playwright TypeScript

Playwright with TypeScript — POM, fixtures, locators.

Pillar guide · 7 articles
More in this cluster
From the Playwright pillar

Keep building your QA edge

Practice these questions live

Rehearse with an AI QA interviewer that scores your answers in real time.

Start a Free AI Mock Interview →

Continue reading

Topic mapConcepts · Tools · People · Standards

Related concepts, tools & standards around Automation Testing

A quick reference of the people, companies, frameworks and technologies most often mentioned alongside Automation Testing in real QA teams — useful when you're mapping a learning path, preparing for interviews, or scoping a new project.

Core testing concepts
Auto-waitingTest FixturesParallel ShardingTest PyramidShift-Left TestingBehavior-Driven DevelopmentTest-Driven DevelopmentPage Object ModelContract TestingExploratory Testing
Testing tools
Programming languages
JavaPythonJavaScriptTypeScriptC#SQL
Certifications worth knowing
ISTQB Foundation LevelISTQB Advanced — Test AnalystISTQB Agile TesterCertified Selenium ProfessionalAWS Certified DevOps EngineerCertified ScrumMaster (CSM)
Companies hiring for this skill
GoogleMicrosoftAmazonMetaNetflixAtlassianThoughtWorksInfosysTCSWipro

Discussion

Ask a question, share your experience, or correct us. Be kind — real people are reading.

Join the QA Community

Connect with fellow testers, share job leads, and get career advice.