Playwright Selectors and Locators

Updated June 2026
Locators are Playwright's mechanism for finding elements on a web page, and choosing the right locator strategy is the single most important decision for writing maintainable, reliable automation scripts. This guide covers every locator type Playwright offers, from the recommended role-based and test ID locators to CSS selectors and XPath, along with filtering, chaining, and best practices for real-world projects.

How Locators Work in Playwright

A locator in Playwright is a strict representation of how to find an element on the page. Unlike older tools that resolve a selector immediately and return a reference to a DOM element, Playwright locators are lazy. They describe which element to find but only query the DOM when an action is performed. This means a locator remains valid even if the page re-renders between when you create the locator and when you use it.

Locators also enforce strictness by default. If a locator matches more than one element, Playwright throws an error rather than silently acting on the first match. This prevents subtle bugs where a selector that used to match one element starts matching two after a UI change, and the test continues passing while operating on the wrong element. You must refine your locator until it uniquely identifies the target element.

Every locator automatically participates in Playwright's auto-waiting mechanism. When you call .click() on a locator, Playwright waits for the element to be visible, stable, enabled, and receiving pointer events before performing the click. This eliminates the flaky behavior caused by acting on elements that are still loading, animating, or covered by overlays.

Role-Based Locators

Role-based locators are Playwright's recommended default strategy. They find elements by their ARIA role and accessible name, which aligns with how assistive technologies and real users perceive the page. This approach produces selectors that are resilient to markup changes because the semantic meaning of an element (a button, a link, a heading) tends to remain stable even when the underlying HTML structure changes.

The page.getByRole() method accepts a role name and optional filtering criteria. page.getByRole('button', { name: 'Submit' }) finds a button element whose accessible name is "Submit." The accessible name comes from the element's text content, aria-label, aria-labelledby, or associated label element, depending on the role.

Common roles you will use frequently include 'button' for clickable actions, 'link' for navigation, 'heading' for section titles (with an optional level filter for specific heading levels), 'textbox' for input fields, 'checkbox' and 'radio' for form controls, 'combobox' for dropdown selects, and 'listitem' for items in a list. The full list of ARIA roles covers virtually every interactive element type in HTML.

The name option supports string matching (exact or substring) and regular expressions. { name: /submit/i } performs a case-insensitive substring match, which is more resilient to minor text changes than exact matching. Use the exact option when you need precise matching: { name: 'Submit', exact: true }.

Role locators also accept state filters: { pressed: true } for toggle buttons, { checked: true } for checkboxes, { expanded: true } for expandable sections, and { disabled: true } for disabled controls. These filters narrow the match to elements in a specific state, which is useful for verifying that UI state changes occurred correctly.

Text Locators

Text locators find elements by their visible text content. page.getByText('Welcome back') matches any element whose visible text contains "Welcome back." The match is case-sensitive and searches for a substring by default, so "Welcome back, John" would also match.

For exact matching, pass the exact option: page.getByText('Welcome back', { exact: true }). This matches only elements whose entire visible text is exactly "Welcome back" with no additional text. Regular expressions provide even more flexibility: page.getByText(/welcome back/i) for case-insensitive matching.

Text locators work best for static text content like headings, labels, error messages, and navigation items. They are less suitable for dynamic content that changes frequently, since any change in the displayed text will break the locator. For internationalized applications where text varies by locale, text locators should be avoided in favor of test IDs or role-based locators with stable accessible names.

A related method is page.getByLabel(), which finds form controls by their associated label text. page.getByLabel('Email address') finds the input element linked to a label containing "Email address," whether the association is through the for attribute, wrapping, or aria-labelledby. This is the preferred way to locate form inputs because it mirrors how users identify form fields.

Test ID Locators

Test ID locators use a dedicated data attribute (default data-testid) to identify elements specifically for automation. page.getByTestId('login-form') finds the element with data-testid="login-form".

The main advantage of test IDs is complete isolation from the visual presentation. A redesign that changes text, CSS classes, DOM structure, and even element types will not break a test ID locator as long as the data-testid attribute survives. This makes test IDs the most stable locator type for projects where the UI changes frequently.

The tradeoff is that test IDs require modifying your application's source code to add the data-testid attributes. This is straightforward for teams that own the application under test, but not possible when automating third-party sites. Some teams resist adding test-specific attributes to production code, though the attributes have no effect on behavior and can be stripped during the build process if desired.

The attribute name is configurable in playwright.config.ts through the testIdAttribute option. If your application already uses a different convention like data-cy (from Cypress) or data-qa, configure Playwright to use that attribute instead of adding a parallel naming scheme.

CSS and XPath Locators

CSS selectors and XPath are available through the general page.locator() method. page.locator('.product-card') uses a CSS selector, and page.locator('xpath=//div[@class="product-card"]') uses XPath. These are the most flexible locator types because they can target any element based on its attributes, position, or relationship to other elements.

CSS selectors in Playwright support the full CSS selector specification including attribute selectors ([data-status="active"]), pseudo-classes (:nth-child(2)), combinators (descendant, child, sibling), and the :has() pseudo-class for parent selection. Playwright also adds custom pseudo-classes: :visible to match only visible elements and :text("content") to match by text content.

XPath is most useful when you need to traverse the DOM upward (from child to parent) or when the target element can only be identified by its relationship to surrounding elements. XPath expressions like //td[text()="Price"]/following-sibling::td find a table cell based on its label, which is difficult to express in CSS.

The downside of CSS and XPath locators is fragility. They depend on DOM structure, CSS class names, and HTML attributes that may change with any UI update. This makes them harder to maintain than role-based or test ID locators. Use them as a fallback when semantic locators cannot identify the target element, not as the default strategy.

Filtering and Chaining Locators

Complex pages often have multiple elements of the same type, and a single locator may match several of them. Playwright provides filtering and chaining methods to narrow results precisely.

The .filter() method adds criteria to an existing locator. page.getByRole('listitem').filter({ hasText: 'Product A' }) finds list items that contain the text "Product A." You can also filter by child elements: .filter({ has: page.getByRole('button', { name: 'Buy' }) }) finds list items that contain a "Buy" button.

The .locator() method chains locators to scope the search. page.locator('.sidebar').getByRole('link', { name: 'Settings' }) first finds the sidebar container, then searches within it for a link named "Settings." This is essential when the same element appears in multiple page sections and you need to target a specific instance.

The .first(), .last(), and .nth(index) methods select a specific element when multiple match. Use these sparingly because position-based selection is fragile, but they are necessary when elements are truly identical and cannot be distinguished by any other criteria.

.and() combines two locators to find elements matching both criteria. page.getByRole('button').and(page.getByText('Submit')) finds elements that are both buttons and contain the text "Submit." This is useful for disambiguation when either criterion alone matches too many elements.

Locator Best Practices

Start with role-based locators for most elements. They are resilient, readable, and align with accessibility best practices. If your application is built with proper semantic HTML and ARIA attributes, role locators cover the vast majority of your element targeting needs.

Fall back to test ID locators for elements without clear roles or accessible names. Custom components, complex widgets, and dynamically generated content often lack the semantic information that role locators need. Adding a data-testid provides a stable target without depending on visual presentation.

Use text locators for static content that defines its purpose, like error messages, headings, and navigation labels. Avoid them for dynamic content that changes based on data or user state.

Reserve CSS and XPath for situations where other strategies fail. Document why the semantic approach did not work so future maintainers understand the decision. If possible, advocate for adding proper ARIA attributes or test IDs to the application instead of relying on structural selectors.

Always verify that your locator matches exactly one element. Playwright's strictness mode helps by throwing errors on ambiguous matches. In development, use the Playwright Inspector to test locators interactively: run npx playwright test --debug and use the "Explore" panel to try selectors against the live page and see exactly which elements they match.

Use the Playwright Codegen tool (npx playwright codegen) to generate locators automatically. Codegen records your interactions with the browser and produces test code with locators. It prefers role-based and text locators, which gives you a good starting point that you can refine manually.

Key Takeaway

Use role-based locators as your default, fall back to test IDs for complex widgets, and reserve CSS and XPath selectors for edge cases. The locator hierarchy of role, then test ID, then text, then CSS ensures your automation code is resilient to UI changes while remaining readable and maintainable.