Locator failures are a common source of test maintenance. A CSS class changes, the DOM is reorganized, or an element moves, and tests fail even though the user flow still works.
Playwright’s getByRole() reduces this dependency on implementation details by locating elements based on their accessibility role and accessible name.
For example, instead of finding a button through its CSS class or DOM position, you can locate the button users know as “Submit”:
await page.getByRole('button', { name: 'Submit' }).click();This can make tests less sensitive to routine UI changes, which means fewer locator fixes and less time spent investigating failures that are unrelated to actual product defects.
By the end of this article, you will understand how getByRole() works, where it fits into a Playwright locator strategy, and how to use it effectively across different testing scenarios.
Why Use GetByRole? Benefits for Test Automation
The impact of brittle locators grows with the test suite. What starts as a few selector fixes can turn into repeated CI failures, debugging work, and delayed feedback for developers.
Using getByRole() can address some of these problems:
- Fewer tests to repair after UI refactoring: A button can move between containers, receive new CSS classes, or have its internal markup changed while remaining the same button to the user. A locator based on its role and accessible name is less likely to break because of those implementation changes. For QA teams maintaining large regression suites, this means less time spent updating selectors after routine frontend work.
- Less time investigating false failures: When a brittle selector breaks, the CI pipeline reports a failed test even though the feature still works. Someone has to reproduce the failure, inspect the trace, identify the broken locator, and update it. More reliable locators reduce this noise, so QA engineers and developers can spend more of their debugging time on failures that may represent actual product problems.
- Faster test reviews and debugging: getByRole(‘button’, { name: ‘Checkout’ }) immediately tells a developer what the test is trying to interact with. A selector such as .checkout-footer > div:nth-child(2) > button does not. Clearer test intent reduces the time needed to understand unfamiliar tests during code reviews and failure investigation.
- Earlier visibility into poor UI semantics: A control that looks like a button but is implemented as a generic <div> may work with a CSS selector while remaining difficult to identify through its intended role. Role-based locators can expose these implementation problems during test development, giving teams a chance to correct the component before accessibility issues spread across the application.
How GetByRole Works: Core Syntax and Options
getByRole() searches for elements based on their accessibility role. You can then narrow the match using the element’s accessible name, state, heading level, and other properties.
The basic syntax is:
page.getByRole(role, options);
For example:
await page.getByRole('button', { name: 'Submit' }).click();Here, button tells Playwright which type of element to find, while name: ‘Submit’ narrows the search to a button with that accessible name.
Common getByRole Options
The name option is the one you will use most often, but Playwright provides additional filters for cases where the element’s state or position in the accessibility hierarchy matters.
- name: Matches the element by its accessible name. It accepts a string or regular expression.
- checked: Filters checkboxes, radio buttons, and other elements that support a checked state.
- disabled: Matches elements based on whether they are disabled.
- pressed: Targets toggle buttons based on their pressed state.
- selected: Filters elements such as options or tabs based on their selected state.
- expanded: Targets controls based on whether they are currently expanded or collapsed.
- includeHidden: Includes elements that are normally excluded because they are hidden from the accessibility tree.
- exact: Controls whether the accessible name must match the provided string exactly.
- level: Filters hierarchical roles such as headings by level.
For example, instead of locating any checkbox named “Notifications,” you can specifically find the one that is already checked:
page.getByRole('checkbox', {
name: 'Notifications',
checked: true
});These options become more useful when a page contains several elements with similar roles and names. Rather than falling back immediately to CSS selectors or positional locators such as .nth(), you can often narrow the locator using information that describes the element’s actual state or purpose.
Implicit vs Explicit ARIA Roles
One common misconception is that getByRole() only works when an element has a role attribute in the HTML. In many cases, the browser already determines the role from the native HTML element.
For example:
<button>Save</button> <a href="/account">Account</a> <h2>Billing Details</h2> <input type="checkbox" />
These elements already have implicit roles:
| HTML element | Implicit role |
|---|---|
| <button> | button |
| <a href=”…”> | link |
| <h2> | heading |
| <input type=”checkbox”> | checkbox |
You can therefore locate the button without adding role=”button”:
page.getByRole('button', { name: 'Save' });This distinction matters when a getByRole() locator fails. Adding a role attribute to make the test pass is not always the right fix. First check whether the element uses the correct native HTML element and what role the browser actually exposes. Adding an incorrect or redundant ARIA role can create an accessibility problem while only masking the real issue in the markup.
How Accessible Names Work with GetByRole
The role tells Playwright what type of element to look for. The accessible name helps identify the specific element you want.
Consider a page with three buttons:
<button>Save</button> <button>Cancel</button> <button>Delete</button>
All three have the button role, so this locator matches more than one element:
page.getByRole('button');Adding the accessible name narrows the locator to the intended button:
page.getByRole('button', { name: 'Save' });The important part is that an accessible name does not always come directly from the text inside an element. Depending on how a control is implemented, its name can come from visible text, an associated <label>, aria-label, or aria-labelledby.
For example, all of the following elements can have an accessible name that Playwright uses when resolving a role-based locator:
<button>Save changes</button> <button aria-label="Close"> <svg>...</svg> </button> <label for="email">Work email</label> <input id="email" type="email"> <h2 id="billing-title">Billing information</h2> <section aria-labelledby="billing-title">...</section>
You could locate them with:
page.getByRole('button', { name: 'Save changes' });
page.getByRole('button', { name: 'Close' });
page.getByRole('textbox', { name: 'Work email' });
page.getByRole('region', { name: 'Billing information' });This is worth checking when a getByRole() locator cannot find an element that appears to have the right text. The visible text and accessible name may be different, so check the element’s computed accessible name before assuming the locator is wrong.
Exact, Partial, and Regular Expression Matching
You do not always need to provide the complete accessible name when using getByRole(). By default, a string can match part of the accessible name and matching is case-insensitive.
For example, this locator can match a button named “Submit order”:
page.getByRole(‘button’, { name: ‘submit’ });
When the page contains similar names, however, a broad match can target more elements than expected. Consider buttons named “Save” and “Save as draft.” If you specifically need the first one, use exact: true:
page.getByRole('button', {
name: 'Save',
exact: true
});Regular expressions are useful when the accessible name can vary while still following a predictable pattern:
page.getByRole('button', { name: /submit/i });Use exact: true when similar element names could create multiple matches. Regular expressions work better when part of the accessible name can change, such as a button that includes a dynamic count. Keep the match only as broad as the UI requires. A loose match can start targeting the wrong element when another control with a similar name is added later.
Practical Examples: Real-World GetByRole Usage
The basic role and name combination covers many everyday interactions. As the UI gets more complex, the other getByRole() options help narrow down elements without relying on CSS selectors or their position in the DOM.
1. Clicking a Button
A button with a clear accessible name can be located directly:
await page.getByRole('button', { name: 'Login' }).click();The locator continues to target the “Login” button even if its CSS class or position on the page changes.
2. Filling Out a Form
Form controls can also be located through their roles and accessible names:
await page.getByRole('textbox', { name: 'Username' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('securePass123');
await page.getByRole('button', { name: 'Submit' }).click();Here, the names depend on how the form fields are labelled. If the visible label and computed accessible name differ, the locator needs to use the accessible name.
3. Working with Checkboxes and Radio Buttons
For controls where state matters, you can use getByRole() both to interact with the element and verify the result:
const terms = page.getByRole('checkbox', { name: 'I agree to Terms' });
await terms.check();
await expect(terms).toBeChecked();The same approach works for radio buttons:
await page.getByRole('radio', { name: 'Credit Card' }).check();4. Locating Headings by Level
A page may contain multiple headings with similar names. The level option lets you target a specific heading level without writing a selector for an <h1>, <h2>, or another heading element.
page.getByRole('heading', {
name: 'Account Settings',
level: 2
});This is also useful when the heading role comes from accessibility semantics rather than the exact HTML tag used in the test.
5. Locating Elements by State
State filters are useful when the state itself identifies the element you need. For example, a settings page may contain several switches with similar purposes, or a navigation component may contain multiple tabs.
To locate the currently selected tab:
page.getByRole('tab', {
name: 'Billing',
selected: true
});To find an expanded control:
page.getByRole('button', {
name: 'Advanced settings',
expanded: true
});You can use the same pattern with checked, pressed, and disabled. These filters are particularly useful when the test needs to interact with an element only in a specific state rather than finding the element first and checking its state separately.
6. Handling Hidden Elements with includeHidden
By default, getByRole() follows ARIA rules when determining which elements are included in role matching. If you intentionally need to locate an element that is normally excluded, you can use includeHidden:
page.getByRole('button', {
name: 'Close',
includeHidden: true
});This option should be used for cases where interacting with or inspecting the hidden element is actually part of the test. If a test needs includeHidden: true just to find a control that a user is expected to interact with, check the element’s visibility and accessibility implementation first.
7. Handling Multiple Elements with the Same Role and Name
Duplicate role and name combinations are common in larger interfaces. A page might have several “Save” buttons inside different forms or dialogs. Making the name more specific is not always possible because the buttons genuinely have the same accessible name.
In that case, scope the search to the part of the page where the interaction takes place:
const dialog = page.getByRole('dialog', {
name: 'Edit profile'
});
await dialog.getByRole('button', { name: 'Save' }).click();This tells Playwright which “Save” button the test means without relying on .first(), .nth(), or a DOM-specific selector. It also keeps the locator tied to the user flow: find the “Edit profile” dialog, then use the “Save” button inside it.
GetByRole vs. Other Locator Strategies
getByRole() is a strong choice for interactive elements with clear roles and accessible names, but it is not the right locator for every element. Playwright provides several user-facing locators, and the best choice depends on how a user identifies the element and what the test needs to verify.
| Locator | Strengths | Limitations | Use Cases |
|---|---|---|---|
| getByRole() | Stable, readable, and based on accessibility semantics | Requires meaningful roles and accessible names; duplicate names can create ambiguous matches | Testing user-facing controls such as buttons, links, headings, tabs, and checkboxes |
| getByLabel() | Directly targets form controls through their associated labels | Requires the label and control to be correctly associated | Testing form interactions such as entering user details, selecting options, and completing checkout fields |
| getByTestId() | Provides an explicit and stable testing contract | Requires dedicated attributes and can hide poor UI semantics when overused | Testing custom or dynamic UI components that do not have a reliable user-facing identifier |
| getByText() | Simple and directly targets content visible to users | Copy changes can break tests; repeated text may create multiple matches | Testing whether user-facing messages, labels, notifications, and other visible content appear correctly |
| CSS Selectors | Precise and can target attributes or DOM structure directly | Often coupled to implementation details and vulnerable to markup changes | Testing element attributes, states, or structural details that cannot be targeted through user-facing locators |
| XPath | Supports complex DOM traversal when other selectors are insufficient | Difficult to read and maintain; highly sensitive to structural changes | Testing legacy applications or complex DOM relationships where more direct locators are not available |
When Not to Use GetByRole
getByRole() is a good fit when users identify an element by its role and accessible name. But forcing every interaction through a role-based locator can make tests harder to understand. Another locator may be a better choice in these situations:
- Testing form fields through their labels: Use getByLabel() when the label is the most natural way to identify an input, such as an email address or shipping address field.
- Checking visible messages or page content: Use getByText() when the test needs to verify that a specific error, confirmation message, or piece of content appears on the page.
- Testing elements without a reliable user-facing identifier: Use getByTestId() when a custom or dynamic component cannot be identified reliably through its role, label, or visible text.
- Validating implementation details: CSS selectors may be appropriate when the test specifically needs to inspect an attribute or DOM relationship rather than reproduce a user interaction.
- Working with legacy applications: XPath or CSS selectors may still be necessary when the application does not expose useful accessibility semantics and changing the underlying markup is not immediately possible.
Use Playwright Codegen to Find the Right Locator
If getByRole() is not suitable for the element you need to test, the next question is which locator to use instead. For elements that do not fit neatly into these cases, choosing the right locator may require some trial and error.
Thankfully Playwright Codegen can solve this. As you interact with the application, it generates locators for the selected elements, giving you a starting point for choosing and refining the locator you use in your test.
Run Codegen against the application you want to test:
npx playwright codegen https://example.com
Suppose you select a login button and Codegen generates:
page.getByRole('button', { name: 'Login' });In this case, you know that the element has a usable role and accessible name, so getByRole() is a good fit. For a form field, Codegen may instead generate:
page.getByLabel('Email address');This gives you a practical way to evaluate locator options without manually trying getByRole(), getByLabel(), getByText(), and other strategies until one works.
The generated locator still needs some judgment. Codegen may produce a locator that works for the current page but is too dependent on position or surrounding markup. If you see something like .nth(1), check whether you can make the target clearer by scoping it to a specific part of the page:
const dialog = page.getByRole('dialog', { name: 'Edit profile' });
await dialog.getByRole('button', { name: 'Save' }).click();Codegen is most useful here as a way to narrow down your locator choice. Use the generated locator to understand how Playwright identifies the element, then check whether that locator still makes sense for the user interaction and page state your test covers.
Conclusion
getByRole() gives Playwright tests a way to locate elements through their role, accessible name, and state instead of depending on CSS classes or DOM structure. To use it reliably, you need to understand the semantics the browser actually exposes, especially when accessible names differ from visible text or several elements share the same role.
The locator you choose also affects how much maintenance the test suite creates later. Use role-based locators where they clearly identify the interaction, scope ambiguous matches instead of relying on position, and use another locator when it better matches what the test is checking.















