Playwright Locators Guide: Every Locator Type Explained with Examples

Playwright locators are the foundation of stable automation. Learn every locator type, when to use them, and how to debug failures without adding manual waits.

Modern web apps load data late and re-render parts of the page often, so the element you want to click might not be ready or might change between steps.

Playwright locators are built for this and are the core of Playwright's auto waiting and retry behavior.

The pain is flaky tests caused by timing and actionability issues. Click happens too early, element is hidden, moving, disabled, or covered, so the test fails unless people start adding manual waits.

This guide fixes that by teaching a clear locator strategy. You will learn which built in locators to prefer, how to chain and filter for accuracy, and how to debug failures using Inspector and traces so your tests stay stable without timeout hacks.

What Are Locators?

Locators are objects that find elements on a page at the moment of interaction, not at the moment of definition. They automatically wait for elements to become visible, enabled, and stable before acting, which eliminates most timing related flaky tests without a single explicit wait.

A quick story that explains why locators matter

A page shows an Add to cart button.

The page updates after you add something, so the screen is rebuilt and the old button is replaced by a new button.

If your test saved the old button earlier, it may fail because that old button no longer exists.

A locator does not save the old button. It saves the rule to find the button again, then Playwright finds the current button and clicks it.

How Locators Differ from Selectors

Most engineers start with selectors because every automation tool uses them. In Playwright, selectors still exist, but they are only the raw input. Locators are the modern layer you should build your test suite around.

Selectors are strings.

Locators are lazy, retrying objects built on top of selectors.

Here is the comparison that matters in day to day work.

Point Selector Locator
Meaning A text you write to find an element A Playwright object you use to find and act on an element
Waiting You often add waits yourself Playwright waits automatically before actions
Retry You handle retry yourself Playwright retries until the timeout
Best for Quick one time targeting Stable clicks, fills, and assertions
When it finds Usually when you first try to use it Right when you click or assert
Narrowing You make the selector longer You chain and filter to be specific
Debug Harder to understand what matched Easier with Inspector and traces
Example 'button.add' page.getByRole('button', { name: 'Add to cart' })

Note: ElementHandles still have a place when you need to evaluate a specific node or read properties. For normal user style actions, locators are the default choice.

How Auto Waiting and Retry Work Under the Hood

When you call a locator action such as click, Playwright runs a series of actionability checks. This is why well written tests can avoid explicit waits.

You ask to click. Playwright checks whether clicking makes sense right now. If it does not, it waits and retries until it does or until the timeout ends.

Actionability checks you should understand:

Attached to DOM — The element exists and is not detached.

Visible — The element is not hidden and has a visible box.

Stable — The element is not moving due to animations or layout shifts.

Receives events — The element is not covered by another element that intercepts the click.

Enabled — The element is not disabled.

Built in Locator Types in Playwright

Playwright gives you seven built in locator methods that cover most stable automation needs. You can think of them as a ladder.

Start with the methods that match user intent. Only fall back to lower level targeting when you have a strong reason.

Playwright provides seven built in locator methods: getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle, and getByTestId. The official recommendation is to prefer getByRole first, fall back to getByText or getByLabel for form fields, and use getByTestId only when semantic locators are not possible.

All examples below are written to match patterns you will find in an ecommerce UI. If a label differs in storedemo, use Pick Locator to confirm the accessible name and swap it in.

1. getByRole: Locate by Accessibility Role

If you want one locator to standardize across a team, start here. Role based locators are easy to read in code review and hard to break during refactors.

Roles often come for free. A button element has role button even without an explicit role attribute. A link element has role link. Headings have role heading.

Common patterns:

product-detail.spec.ts
// Product detail page: click Add to Cart button
await page.getByRole('button', { name/add to cart/i }).click();
// Navigation: click a link by role
await page.getByRole('link', { name/all products/i }).click();
// Assert a heading is visible
await expect(page.getByRole('heading', { name/all products/i })).toBeVisible();

If your UI has filters, checkboxes are also easy to target:

filters.spec.ts
const inStock = page.getByRole('checkbox', { name/in stock/i });
if (await inStock.count()) {
  await inStock.check();
}

Exact matching matters when the UI contains similar labels:

navigation.spec.ts
await page.goto('https://storedemo.testdino.com/products');
await page.getByRole('link', { name'Rode NT1-A Condenser Mic' }).click();

2. getByText: Locate by Visible Text Content

Text locators are best for assertions and content checks. They can also be used for clicks, but use them carefully because text changes with localization, A B tests, and product copy edits.

Three matching styles you will use:

cart.spec.ts
await expect(page.getByText('Added to cart', { exacttrue })).toBeVisible();
await expect(page.getByText('Added', { exactfalse })).toBeVisible();
await expect(page.getByText(/added\s+to\s+cart/i)).toBeVisible();

A common pitfall is matching the wrong element because a parent container includes the text. When this happens, scope first, then match text inside the scope.

product-card.spec.ts
const productCard = page.getByRole('listitem').filter({ hasText'Dino Plush' });
await expect(productCard.getByText('Dino Plush', { exacttrue })).toBeVisible();
await productCard.getByRole('button', { name/add to cart/i }).click();

Tip: If getByText matches too many elements, do not reach for nth immediately. Scope to a container, then search inside it.

3. getByLabel: Locate Form Controls by Label

If a field has a label, getByLabel is usually the strongest choice because it follows real accessibility relationships.

Playwright can resolve labels through:

  • label for and id mapping
  • label wrapping the input
  • aria labelledby

Login form example:

contact-us.spec.ts
await page.goto('https://storedemo.testdino.com/contact-us');
await page.getByLabel(/first name/i).fill('qa');
await page.getByLabel(/last name/i).fill('example');
await page.getByLabel(/subject/i).fill('Playwright locators');
await page.getByLabel(/your message/i).fill('Testing getByLabel on storedemo.');
await page.getByRole('button', { name/send message/i }).click();

4. getByPlaceholder: Locate Inputs by Placeholder Text

Placeholders are a useful fallback, especially for search boxes. They are also less accessible than labels, so treat this as a second choice.

search.spec.ts
await page.goto('https://storedemo.testdino.com/products');
const search = page.getByPlaceholder(/search products/i);
await search.fill('headphones');
await search.press('Enter');
await expect(page.getByText(/headphones/i).first()).toBeVisible();

5. getByAltText: Locate Images and Areas

Alt text makes images accessible and testable. When alt text is meaningful, image targeting becomes stable.

product-image.spec.ts
await page.goto('https://storedemo.testdino.com/products');
// Product images have alt text matching their names
await page.locator('a[href*="/product/"]').first().click();
await expect(page.getByAltText(/rode|condenser|mic/i)).toBeVisible();

6. getByTitle: Locate by Title Attribute

Title attributes appear less often in modern apps, but they still show up in icon buttons and tooltip based UIs.

icon-button.spec.ts
await page.getByTitle(/cart|open cart|settings/i).click();

7. getByTestId: The Explicit Testing Contract

When semantic locators are not viable, test ids give you a stable contract. This does require coordination. QA cannot invent stability if the UI has no stable signals.

By default, getByTestId uses data-testid.

button.html
<button data-testid="add-to-cart-button">Cart</button>

cart.spec.ts
await page.getByTestId('add-to-cart-button').click();

If your team already uses a different attribute, configure it once.

playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    testIdAttribute'data-pw'
  }
});

Note: If you add test ids, make them meaningful. Do not encode layout or styling. Encode intent, like checkout submit, cart open, product card, filter toggle.

CSS and XPath Locators in Playwright

Playwright supports CSS and XPath through page.locator. You can use them, but you should not treat them as equal alternatives to built in locators.

CSS and XPath follow DOM structure. Your users do not.

When the page layout changes but the button still does the same job, role and label locators usually keep working, but CSS or XPath paths often stop working.

Playwright supports CSS and XPath selectors through page.locator(), but the official recommendation is to avoid them as your primary locator strategy. CSS and XPath selectors are tied to DOM structure, which means they break when the UI changes, even if the user facing behavior stays identical.

Same element located three ways, in the order you should prefer:

locator-comparison.spec.ts
await page.getByRole('button', { name/add to cart/i }).first().click();
await page.locator('button:has-text("Add to cart")').first().click();
await page.locator('xpath=//button[contains(., "Add to cart")]').first().click();

1. CSS Selectors: Syntax and Practical Examples

Playwright adds CSS extensions that are worth learning:

  • has-text()

  • has()

  • visible

Examples you can adapt to storedemo:

css-selectors.spec.ts
await page.locator('button:has-text("Add to cart")').first().click();
const card = page.locator('article:has-text("Dino Plush")');
await card.locator('button:has-text("Add to cart")').click();
await page.locator('button:has-text("Add to cart"):visible').first().click();
await page.locator('[data-testid="cart-open"]').click();

2. XPath Selectors: When and Why to Use Them

XPath can traverse up to an ancestor. That can help in legacy markup, but it becomes hard to maintain if it grows into a long path.

A legitimate case:

xpath-example.spec.ts
const xpath =
  'xpath=//h3[contains(., "Dino Plush")]/ancestor::*[self::article or self::div][1]//button[contains(., "Add to cart")]';
await page.locator(xpath).click();

A more maintainable locator chain:

chained-locator.spec.ts
const product = page.locator('article').filter({ hasText'Dino Plush' });
await product.getByRole('button', { name/add to cart/i }).click();

Filtering and Chaining Locators

Once you know each locator type, the next step is precision. Real pages contain repeated elements, similar buttons, and duplicated text. Filtering and chaining lets you stay strict without falling back to layout and index hacks.

Locator chaining in Playwright narrows the search scope by combining multiple locators, first finding a parent container, then locating a specific child within it. This produces precise, readable selectors without fragile CSS or XPath paths.

Filtering by Text and Child Locators

filter-by-text.spec.ts
const productName = 'Dino Plush';
const product = page.getByRole('listitem').filter({ hasTextproductName });
await product.getByRole('button', { name/add to cart/i }).click();
await expect(page.getByRole('link', { name/cart/i })).toBeVisible();

Filter by child locator:

filter-by-child.spec.ts
const rowWithRemove = page.locator('tr').filter({
  haspage.getByRole('button', { name/remove/i })
});
await expect(rowWithRemove).toBeVisible();

Filter by not having text:

filter-not-text.spec.ts
const available = page.locator('article').filter({ hasNotText/out of stock/i });
await expect(available.first()).toBeVisible();

Chaining Locators for Nested Elements

chained-nested.spec.ts
const card = page.locator('article').filter({ hasText'Dino Plush' });
await card.getByRole('button', { name/add to cart/i }).click();

A form example that stays readable:

checkout.spec.ts
const checkout = page.getByRole('main');
await checkout.getByLabel(/email/i).fill('[email protected]');
await checkout.getByRole('button', { name/continue|pay|place order/i }).click();

Which Locator Should You Use?

A good locator strategy is not a list of options. It is a ranked decision you can apply the same way every time.

The recommended Playwright locator priority is: (1) getByRole for interactive elements, (2) getByLabel for form fields, (3) getByText for static content, (4) getByTestId for elements without semantic attributes, (5) CSS and XPath only as a last resort for legacy DOM structures.

If your team wants a broader process around this, connect it to your testing standards. This is where end to end testing best practices helps reviewers enforce the same rules across the suite.

Common UI Elements and Their Best Locators

UI element Recommended locator Example
Primary button getByRole page.getByRole('button', { name: /checkout/i })
Navigation link getByRole page.getByRole('link', { name: /cart/i })
Text input with label getByLabel page.getByLabel(/email/i)
Search input without label getByPlaceholder page.getByPlaceholder(/search/i)
Toast or banner message getByText page.getByText(/added to cart/i)
Product image or logo getByAltText page.getByAltText(/logo/i)
Icon with tooltip getByTitle page.getByTitle(/settings/i)
Custom widget getByTestId page.getByTestId('shipping-method')

Debugging Locators That Do Not Work

When a locator fails, guessing wastes time. A fast debug loop is systematic. You reproduce, verify what the locator matches, then you fix the root cause.

When a Playwright locator fails, use the Playwright Inspector with the debug flag to live edit locators and see which elements they match in real time. For intermittent failures, use the Trace Viewer to replay the exact DOM state at the moment of failure.

In the given below screenshot you can navigate to the Playwright inspector/pick locator

To enable the traces of your tests use the below command:

terminal
npx playwright show-trace path/to/trace.zip

At this point in the guide, the reader already understands the pain. If you want to turn every failing run into a quick fix, you need traces and history. TestDino helps teams track failures and rerun trends in one place.

Stop reruns, fix root cause
Track failures across runs in one place
Open TestDino CTA Graphic

Using Playwright Inspector to Test Locators

Step by step workflow:

1. Run the test in debug mode:

terminal
npx playwright test --debug

2. Use Pick Locator and click the element you want to target.

3. Compare the suggested locator with your locator.

4. If the locator matches multiple elements, scope it using a parent container and filter.

5. If the click fails, inspect the actionability log to see whether an overlay blocked it.

6. Use highlight to see exactly what the locator matches.

debug.spec.ts
const addToCart = page.getByRole('button', { name/add to cart/i });
await addToCart.highlight();
await addToCart.click();

Trace Viewer for Intermittent Locator Failures

Configure tracing and artifacts:

playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    trace'on-first-retry',
    screenshot'only-on-failure',
    video'retain-on-failure'
  }
});

Open the trace after a failure:

terminal
npx playwright show-trace test-results

How to read the trace:

  1. Click the failing step

  2. Inspect the DOM snapshot at that moment

  3. Check whether the locator matches zero elements or too many

  4. Check whether an overlay blocks the click

  5. Update the locator policy, not just this single test

If you are actively reducing flakiness, flaky test detection helps you separate unstable selectors from real regressions.

Debug CI failures fast
Centralize traces, videos, screenshots
Try TestDino CTA Graphic

Playwright Locator Best Practices

Every best practice below includes a wrong pattern and a better pattern.

The three most impactful Playwright locator best practices are:

(1) always prefer getByRole over CSS selectors,

(2) never use first or nth to resolve ambiguous locators, fix the locator instead, and

(3) use getByTestId as a stable fallback when semantic locators are not viable.

Practices That Prevent Flaky Tests

1. Prefer getByRole for interactive elements

Not that:

anti-pattern.spec.ts
await page.locator('button.primary').click();

Do this:

best-practice.spec.ts
await page.getByRole('button', { name/checkout/i }).click();

2. Fix ambiguity instead of using nth by default

Not that:

anti-pattern.spec.ts
await page.getByText('Edit').nth(2).click();

Do this:

best-practice.spec.ts
const row = page.getByRole('row').filter({ hasText'[email protected]' });
await row.getByRole('button', { name/edit/i }).click();

3. Replace fixed waits with state based assertions

Not that:

anti-pattern.spec.ts
await page.waitForTimeout(2000);

Do this:

best-practice.spec.ts
await expect(page.getByText(/added to cart/i)).toBeVisible();

4. Avoid storing broad locators across navigation

Not that:

anti-pattern.spec.ts
const button = page.locator('button');
await page.goto('https://storedemo.testdino.com/checkout');
await button.click();

Do this:

best-practice.spec.ts
await page.goto('https://storedemo.testdino.com/checkout');
await page.getByRole('button', { name/continue|pay/i }).click();

5. Scope before you click

Not that:

anti-pattern.spec.ts
await page.getByText('Add to cart').click();

Do this:

best-practice.spec.ts
const card = page.getByRole('listitem').filter({ hasText'Dino Plush' });
await card.getByRole('button', { name/add to cart/i }).click();

Practices That Improve Test Maintainability

A maintainable suite is also about where you put locators so the whole suite benefits.

A practical pattern is a small Page Object that exposes intent based actions.

store-page.ts
import { PageLocator } from '@playwright/test';
export class StorePage {
  readonly pagePage;
  readonly cartLinkLocator;
  readonly searchBoxLocator;
  constructor(pagePage) {
    this.page = page;
    this.cartLink = page.getByRole('link', { name/cart/i });
    this.searchBox = page.getByPlaceholder(/search/i);
  }
  productCard(namestring) {
    return this.page.getByRole('listitem').filter({ hasTextname });
  }
  async addToCart(productNamestring) {
    await this.productCard(productName)
      .getByRole('button', { name/add to cart/i })
      .click();
  }
}

If you want to evaluate stability at the suite level, start with Playwright test reports and keep your review process consistent.

See test history at scale
Turn runs into searchable insights
View TestDino CTA Graphic

Generating Locators with Playwright Codegen

Codegen is best used as a discovery tool. Use it to learn what Playwright thinks is stable, then refactor the output to match your policy.

Playwright's codegen tool automatically generates locators by analyzing the page and choosing the most stable selector, prioritizing role, text, and test id locators over CSS. Use it as a discovery tool, then refactor the output to follow locator best practices.

Step by step:

1. Launch codegen against the demo store:

terminal
npx playwright codegen https://storedemo.testdino.com/

2. Record one realistic flow:

  • Search for a product

  • Add to cart

  • Open cart

  • Start checkout

3. Copy the generated code into a new spec file.

4. Refactor the locators:

  • Replace long CSS chains with getByRole and scoping

  • Replace nth with chaining and filter

  • Move repeated patterns into a Page Object

Before:

generated.spec.ts
await page.locator('text=Cart').click();
await page.locator('button:has-text("Add to cart")').nth(0).click();

After:

refactored.spec.ts
await page.getByRole('link', { name/cart/i }).click();
await page.getByRole('button', { name/add to cart/i }).first().click();

Conclusion

Your Playwright suite is only as stable as its locators. Treat selectors as a strategy, not strings.

Default to intent based locators: getByRole() for interactive elements, getByLabel() for form fields, and getByText() mostly for assertions. Use getByTestId() when semantics are missing. Keep CSS and XPath last, and keep them scoped.

If a locator is ambiguous, scope and chain it instead of using first() or nth(). Pair it with web first assertions so Playwright waits correctly instead of relying on timeouts.

Debug the same way every time: Inspector, confirm matches, actionability log, then traces for intermittent failures.

Next step: pick one critical flow, rewrite its locators using this order, and make it your team standard.

Frequently Asked Questions

What is the difference between a locator and a selector in Playwright?
A selector is a string that describes how to find elements. A locator is an object built on top of selectors that resolves at interaction time and adds auto waiting, retry behavior, and debugging support. In practice, this means locators are less brittle on modern UIs that rerender.
What is the best locator strategy in Playwright?
Use getByRole first for interactive elements, then getByLabel for form fields, then getByText for static content assertions, then getByTestId when semantics are not available. Use CSS and XPath only as a last resort for legacy DOM structures.
How do you chain locators in Playwright?
Start with a parent container locator, then locate the child inside it. For example, find the product card that contains a product name, then locate the Add to cart button inside that card. Chaining keeps selectors readable and avoids brittle DOM paths.
How do you debug a Playwright locator that cannot find an element?
Run with the debug flag to open Inspector, use Pick Locator to validate targeting, check the actionability log to see why an action is blocked, and use highlight to confirm what matches. For intermittent failures, record a trace and inspect the DOM snapshot at the failing step.
Can you use XPath in Playwright?
Yes. You can use it via page.locator with an xpath selector. It is supported, but it is not recommended as a primary strategy because it is tightly coupled to DOM structure and usually harder to maintain than locator chaining.
What is getByTestId in Playwright?
getByTestId locates elements by a dedicated testing attribute, data-testid by default. It is the most stable fallback when role, label, and text locators are not viable, but it requires a shared naming policy between developers and QA.
Dhruv Rai

Product & Growth Engineer

Dhruv Rai is a Product and Growth Engineer at TestDino, focusing on developer automation and product workflows. His work involves building solutions around Playwright, CI/CD, and developer tooling to improve release reliability.

He contributes through technical content and product initiatives that help engineering teams adopt modern testing practices and make informed tooling decisions.

Get started fast

Step-by-step guides, real-world examples, and proven strategies to maximize your test reporting success