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.

You've probably been there. You write a test, it passes locally, and then it breaks in CI because the button you were clicking hadn't finished rendering yet. Or worse, you're targeting a CSS selector like div.main > ul > li:nth-child(3) > a and someone on the front-end team shuffles the layout.

This is exactly why Playwright locators exist. They're not just selectors with a fancy name. Locators are objects that find elements at interaction time, wait for them to be ready, and retry if something goes wrong. No more await page.waitForSelector() followed by a prayer.

In this guide, we'll cover every Playwright locator type, show you when to use each one, and walk through the changes that came with Playwright v1.59 and v1.60. If you've got existing tests using _react or _vue selectors, you'll want to read the migration section too.

How Locators Differ from Selectors

What Are Locators?

A locator is a way to find one or more elements on a page. But it's not just a selector string. A Playwright locator is an object that:

  • Resolves lazily - It doesn't search the DOM when you create it. It waits until you actually need to interact with an element.
  • Auto-waits - Before clicking, typing, or doing anything, Playwright checks that the element is attached, visible, stable, enabled, and able to receive events.
  • Retries automatically - If an element isn't ready, Playwright keeps trying until the timeout (30 seconds by default).

This three-part behavior is what makes locators different from raw selectors.

You don't need to write like await page.waitForSelector('.submit-btn') before clicking. Just use a locator and Playwright handles the timing. To understand how the test runner queries the browser's accessibility tree, read our breakdown of Playwright architecture.

locator-vs-selector.spec.ts
// Old way (selector + manual wait)
await page.waitForSelector('.submit-btn');
await page.click('.submit-btn');

// Locator way (auto-waits built in)
await page.getByRole('button', { name: 'Submit' }).click();

How Auto Waiting and Retry Work Under the Hood

Every time you call an action on a locator (like .click() or .fill()), Playwright runs through this sequence:

What Changed in Playwright Locators (v1.58 to v1.60)

Recent Playwright releases in 2026 introduced key modifications to locator engines. Here is a summary of major removals and additions:

Removals in v1.58

  • Removed Selectors: The framework-specific _react and _vue selector engines, along with the Shadow DOM-piercing :light suffix, have been completely removed.
  • Migration: Switch to user-facing locators (getByRole(), getByTestId()) or standard CSS selectors.

migration-v158.spec.ts
// Before (Fails in v1.58+)
await page.locator('_react=BookItem[author="Kafka"]').click();

// After (v1.58+)
await page.getByRole('listitem').filter({ hasText: 'Kafka' }).click();

Additions in v1.59

  • page.pickLocator(): Launches a visual element picker in headed mode (--headed) to automatically generate the best locator string by clicking elements on the page.
  • locator.normalize(): A refactoring tool that converts brittle CSS/XPath selectors into clean, ARIA-based or test-ID canonical locators.
  • getByRole() description option: Matches elements by their accessible description (e.g., aria-describedby or title attributes) for more precise targeting:

description-option.spec.ts
await page.getByRole('button', { name: 'Submit', description: 'Sends form data' }).click();

Additions in v1.60

  • locator.drop(): Simulates external file drops or clipboard data transfers onto dropzones.

drop-example.spec.ts
await page.locator('#dropzone').drop({
  files: { name: 'report.pdf', mimeType: 'application/pdf', buffer: Buffer.from('data') }
});

  • ARIA Snapshots: Adds boxes: true and mode: 'ai' to ariaSnapshot(), and allows expect(page).toMatchAriaSnapshot() directly on page objects.

Built in Locator Types in Playwright

1. getByRole: Locate by Accessibility Role

getByRole() matches elements by their ARIA role. Most HTML elements have implicit roles: <button> has role "button", <a> has role "link", <input type="checkbox"> has role "checkbox", and so on.

getbyrole-basics.spec.ts
// Click a button by its visible text
await page.getByRole('button', { name: 'Sign Up' }).click();

// Click a link
await page.getByRole('link', { name: 'Documentation' }).click();

// Check a checkbox
await page.getByRole('checkbox', { name: 'Accept terms' }).check();

// Select a tab
await page.getByRole('tab', { name: 'Settings' }).click();

getByRole Options

The name option matches the element's accessible name (the text users see or screen readers announce). You can use exact matching or regex:

getbyrole-name-option.spec.ts
// Exact match (default)
await page.getByRole('button', { name: 'Submit' }).click();

// Substring match
await page.getByRole('button', { name: /submit/i }).click();

// With description (NEW in v1.59)
await page.getByRole('button', {
  name: 'Delete',
  description: 'Permanently removes this item'
}).click();

Exact matching is critical when the UI contains elements with similar labels. For example, to open a specific product page:

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

Below is the Playwright Inspector showing the exact match link resolution:

Other useful options:

  • exact: true for case-sensitive, whole-string matching
  • pressed: true for toggle buttons
  • expanded: true for accordions/dropdowns
  • checked: true for checkboxes (already checked)
  • level: 2 for headings (matches <h2>)

2. getByText: Locate by Visible Text Content

getByText() matches elements by their text content. It's best for assertions and clicking non-interactive elements like headings, paragraphs, or list items.

getbytext.spec.ts
// Check that a heading exists
await expect(page.getByText('Getting Started')).toBeVisible();

// Click a menu item
await page.getByText('Account Settings').click();

// Use regex for flexible matching
await page.getByText(/welcome/i).click();

By default, getByText() does a substring match. Use { exact: true } for an exact match:

getbytext-exact.spec.ts
// This matches "Submit Form" and "Submit"
page.getByText('Submit');

// This matches only "Submit" exactly
page.getByText('Submit', { exact: true });

3. getByLabel: Locate Form Controls by Label

getByLabel() finds form controls by their associated label text. It works with both <label for="..."> and wrapping <label> patterns.

For example, filling out a contact form on TestDino:

contact-form.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();

Here is how Playwright locates and fills these form elements in real-time during test execution:

4. getByPlaceholder: Locate Form Fields by Placeholder

Some forms skip labels and rely on placeholder text instead (not great for accessibility, but common). getByPlaceholder() handles these cases.

getbyplaceholder.spec.ts
await page.getByPlaceholder('Search...').fill('playwright locators');
await page.getByPlaceholder('Enter your email').fill('test@example.com');

5. getByAltText: Locate Images and Areas

Use getByAltText() to locate images, area elements, or custom graphics that have an alternate text description. This ensures that screen readers and other assistive technologies can identify the element.

getbyalttext.spec.ts
// Images with alt text
await page.getByAltText('Company logo').click();

6. getByTitle: Locate by Title Attribute

Use getByTitle() to locate elements that have a title attribute, which typically displays as a tooltip when a user hovers over the element.

getbytitle.spec.ts
// Elements with title attribute
await page.getByTitle('Close dialog').click();

7. getByTestId: The Explicit Testing Contract

When semantic locators don't work (maybe the element has no meaningful role, label, or text), getByTestId() is your safety net. It matches the data-testid attribute.

getbytestid.spec.ts
// In your HTML: <button data-testid="add-to-cart-button">Cart</button>
await page.getByTestId('add-to-cart-button').click();

Here is the targeted element in the Playwright Inspector:

While test IDs are highly stable, using semantic locators first helps to reduce test maintenance and ensures your tests reflect real user accessibility.

You can customize which attribute Playwright uses for test IDs in your config:

playwright.config.ts
export default defineConfig({
  use: {
    testIdAttribute: 'data-qa'// or whatever your team uses
  },
});

CSS and XPath Locators in Playwright

You can still use CSS and XPath selectors with page.locator(). They work, but they're more brittle because they depend on DOM structure.

1. CSS Selectors: Syntax and Practical Examples

Use CSS selectors to target elements based on their class names, IDs, attributes, or structural relationships in the DOM tree. CSS selectors are standard in web development but should be treated as a last resort in testing compared to user-facing locators.

css-selector.spec.ts
// CSS selector
await page.locator('.nav-menu > .dropdown-item:first-child').click();

Removed in v1.58: _react, _vue, and :light Selectors

Playwright v1.58 dropped three selector engines that some teams relied on:

  • _react selector - Used to find React components by component name and props. Gone.
  • _vue selector - Same idea, but for Vue components. Also gone.
  • :light suffix - Used to opt out of Shadow DOM piercing. No longer supported.

If your tests used any of these, they will break after upgrading. The fix is straightforward: switch to user-facing locators like getByRole(), getByTestId(), or standard CSS selectors.

Before (v1.57 and earlier):

legacy-selectors.spec.ts
// These no longer work in v1.58+
await page.locator('_react=BookItem[author="Kafka"]').click();
await page.locator('_vue=book-list').click();
await page.locator('button:light').click();

After (v1.58+):

migrated-selectors.spec.ts
// Use user-facing locators instead
await page.getByRole('listitem').filter({ hasText: 'Kafka' }).click();
await page.getByTestId('book-list').click();
await page.locator('button').click(); // standard CSS, no :light needed

The Playwright team made this change because _react and _vue selectors were framework-specific and broke easily between framework versions. User-facing locators are more stable because they match what your users actually see and interact with.

If you are migrating legacy tests from older frameworks or selectors, check out our Selenium to Playwright migration guide.

2. XPath Selectors: When and Why to Use Them

XPath (XML Path Language) allows you to traverse the XML structure of a document. It is useful for complex DOM traversal, such as finding elements based on their text content or parent-child relationships that are hard to target with CSS.

xpath.spec.ts
// XPath
await page.locator('//div[@class="modal"]//button[text()="Confirm"]').click();

Filtering and Chaining Locators

You can combine locators to narrow down results.

Filtering by Text and Child Locators

filter() narrows results based on text content or child elements:

filter.spec.ts
// Find a list item containing "Kafka"
await page.getByRole('listitem')
  .filter({ hasText: 'Kafka' })
  .click();

// Find a row that contains a specific button
await page.getByRole('row')
  .filter({ has: page.getByRole('button', { name: 'Edit' }) })
  .click();

Chaining Locators for Nested Elements

Chaining scopes a locator within another locator's subtree:

chaining.spec.ts
// Find the "Delete" button inside the "User Settings" section
await page.getByRole('region', { name: 'User Settings' })
  .getByRole('button', { name: 'Delete' })
  .click();

Logical Operators (.or() and .and())

Sometimes you need to match one of several possible locators:

logical-operators.spec.ts
// Match either a button OR a link with "Submit"
const submitElement = page.getByRole('button', { name: 'Submit' })
  .or(page.getByRole('link', { name: 'Submit' }));
await submitElement.click();

// Match an element that's both a button AND has specific text
const specificButton = page.getByRole('button')
  .and(page.getByText('Save Changes'));
await specificButton.click();

Which Locator Should You Use?

Common UI Elements and Their Best Locators

Playwright gives you several locator types. Here's the priority order the Playwright team recommends:

Locator When to Use
getByRole() Interactive elements (buttons, links, checkboxes, inputs)
getByLabel() Form fields with labels
getByPlaceholder() Inputs with placeholder text
getByText() Static text content, headings
getByAltText() Images with alt text
getByTitle() Elements with title attribute
getByTestId() When no semantic locator works
CSS / XPath Legacy DOM structures only

Start at the top and work your way down. getByRole() is almost always your best bet because it matches what assistive technologies (screen readers) see, which means it's stable across visual redesigns.

Debugging Locators That Do Not Work

When tests fail because a locator can't find an element, here are the tools that help:

Using Playwright Inspector to Test Locators

Run tests with --debug to open the Playwright Inspector:

terminal
npx playwright test --debug

The inspector lets you step through actions, see which elements locators resolve to, and test locators interactively.

Visual Element Picker (page.pickLocator()) (v1.59+)

This is a helpful tool for test development. Run your tests in headed mode, and pickLocator() lets you click on any element in the browser to generate the best locator for it automatically.

pick-locator.spec.ts
// Run with --headed flag
const locator = await page.pickLocator();
console.log(locator); // Outputs something like: getByRole('button', { name: 'Submit' })
await locator.click();

You need headed mode for this (npx playwright test --headed). It won't work in headless CI runs, but it's perfect for local test creation. To activate it, click the Pick Locator button in the Playwright Inspector toolbar, as shown below:

Brittle Selector Cleanup (locator.normalize()) (v1.59+)

Got a codebase full of CSS selectors that break every time the UI changes? normalize() converts them to best-practice locators automatically.

normalize.spec.ts
// Start with a brittle selector
const brittle = page.locator('[data-testid="header-cart-icon"]');

// Convert to canonical form
const clean = brittle.normalize();
// Result: getByTestId('header-cart-icon')

It prioritizes ARIA roles, test IDs, and stable attributes. Run this across your test suite to get cleaner, more resilient selectors.

Trace Viewer for Intermittent Locator Failures

Playwright's trace viewer records screenshots, DOM snapshots, network activity, and console logs for every test action. When a locator fails, the trace shows you exactly what the page looked like at that moment. Before setting up advanced configurations, read our detailed Playwright debugging guide.

playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry',
  },
});

After a failure, open the trace with:

terminal
npx playwright show-trace trace.zip

You can find complete setup steps in our Playwright Trace Viewer guide.

If you're running tests at scale in CI, tools like TestDino parse Playwright traces automatically. Instead of downloading and opening trace files one by one, you see screenshots, network logs, and console errors in one dashboard. TestDino's AI failure analysis also tells you whether a failure was a real bug, an infrastructure problem, or a flaky test, so you know where to focus.

Playwright Locator Best Practices

Practices That Prevent Flaky Tests

  1. Start with getByRole() - It's the most stable option and matches what users actually see. Use the description option (v1.59+) when names alone aren't enough.
  2. Avoid structure-dependent selectors - div > ul > li:nth-child(3) breaks when the UI evolves. Prefer user-facing attributes.
  3. Use getByTestId() as a fallback, not a default - Test IDs are stable, but they don't test accessibility. Try semantic locators first.
  4. Remove _react, _vue, and :light selectors - These were removed in v1.58 and will cause test failures if you upgrade Playwright without migrating.

Practices That Improve Test Maintainability

  1. Run normalize() on your existing selectors - If you inherited a test suite full of CSS selectors, normalize() can suggest better alternatives across your whole codebase.
  2. Use pickLocator() during test development - Instead of guessing which locator works best, let Playwright suggest one by clicking the element directly.
  3. Chain and filter for precision - Instead of complex CSS selectors, combine simple locators: section.getByRole('button', { name: 'Delete' }).
  4. Use Page Object Model - Combine your selectors with the Playwright Page Object Model pattern to keep locator definitions dry.

External Drag-and-Drop (locator.drop()) (v1.60)

Simulating file drops onto upload zones can be difficult. The drop() method simulates external data (files, clipboard content) being dropped onto an element.

drop-files.spec.ts
// Drop files onto an upload zone
await page.locator('#dropzone').drop({
  files: {
    name: 'report.pdf',
    mimeType: 'application/pdf',
    buffer: Buffer.from('file content here')
  }
});

// Drop text/URL data
await page.locator('#dropzone').drop({
  data: {
    'text/plain''hello world',
    'text/uri-list''https://example.com'
  }
});

This dispatches dragenter, dragover, and drop events with a proper DataTransfer object.

ARIA Snapshot Improvements (v1.60)

Version 1.60 also added ariaSnapshot({ boxes: true }) for bounding box data and ariaSnapshot({ mode: 'ai' }) for AI-optimized snapshots. Plus, you can now use expect(page).toMatchAriaSnapshot() directly on page objects, not just locators.

These changes matter most for teams building AI-powered testing workflows. If your team is exploring AI-powered QA, take a look at our analysis of the Playwright AI ecosystem.

Generating Locators with Playwright Codegen

Playwright Codegen is a powerful code-generation tool that lets you record manual browser interactions and automatically outputs test code with stable locators.

You can launch the generator from your command line:

terminal
npx playwright codegen

As you click around the page, Codegen analyzes the DOM and prioritizes stable, user-facing locators (like getByRole, getByText, and getByTestId) over fragile CSS classes.

While Codegen is an excellent tool for discovering how Playwright wants you to locate elements, treat its output as a starting point. Always review and refactor the generated locators to ensure they follow your project's clean testing policies and best practices.

Conclusion

Playwright locators keep getting better. The v1.58-1.60 releases cleaned out legacy selectors and added practical tools like pickLocator() and normalize() that save real time during test development.

If you're starting fresh, stick with getByRole() as your default. If you're maintaining an older test suite, run normalize() across your selectors and replace any _react, _vue, or :light usage before they break.

If you are evaluating how to track test health across your team, compare the top options in our roundup of Playwright reporting tools. And if you're running Playwright tests at scale in CI, you can follow the TestDino integration guide to hook up your pipeline. TestDino categorizes failures automatically so you're not digging through traces to figure out whether a failed locator was a real bug or just a flaky selector.

Frequently Asked Questions

What is the best locator strategy in Playwright?
Use getByRole() first for interactive elements, then getByLabel() for form fields, then getByText() for static content, then getByTestId() when semantic options aren't available. Use CSS and XPath only as a last resort for legacy DOM structures.

What is pickLocator() in Playwright?
Added in v1.59, page.pickLocator() is an interactive API that lets you visually click on any element in a headed browser to generate the best locator for it. It's a development tool - you run tests with --headed, call pickLocator(), click an element, and get back a locator string you can use in your tests.

How do you use locator.normalize() in Playwright?
locator.normalize() (added in v1.59) converts an existing locator into its best-practice canonical form. It prioritizes ARIA roles, test IDs, and stable attributes. For example, page.locator('[data-testid="cart"]').normalize() might return getByTestId('cart'). Use it to clean up brittle selectors in bulk.

What is locator.drop() in Playwright?
Added in v1.60, locator.drop() simulates external drag-and-drop operations, like dropping files onto an upload zone. It dispatches dragenter, dragover, and drop events with a synthetic DataTransfer object. This is different from locator.dragTo(), which moves one on-page element to another.

How does Playwright auto-wait for locators?
When you perform an action on a locator (like .click()), Playwright automatically waits for the element to be attached to the DOM, visible, stable (not moving), able to receive events (no overlay blocking it), and enabled. If any check fails, it retries until the action succeeds or the timeout (default 30 seconds) expires. By ensuring the element is ready before acting, auto-waiting prevents timing issues that lead to Playwright flaky tests in CI/CD environments.

What's the difference between getByRole() name and description?
The name option matches an element's accessible name (visible text or aria-label). The description option (added in v1.59) matches the accessible description (set via aria-describedby or title). Use description when multiple elements share the same name but serve different purposes.
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