🎯 Mastering getByRole in Playwright: Write Reliable and Accessible Tests

When writing UI tests, choosing the right selector can make or break your test suite. Fragile selectors like class names or IDs often lead to flaky tests that break with minor UI changes. That’s where getByRole comes in—a powerful, accessibility-first query method that helps you write robust, readable, and maintainable tests.
In this post, we’ll explore how getByRole works, why it’s better than traditional selectors, and how to use it effectively in your Playwright tests.
🔍 What is getByRole?
getByRole is a query method from the Testing Library family, integrated into Playwright via @playwright/experimental-ct-* packages. It allows you to select elements based on their ARIA roles, which describe the purpose of an element in the UI.
âś… Why Use getByRole?
Here are some compelling reasons to use getByRole:
Accessibility-first: Encourages semantic HTML and accessible components.
Resilient to UI changes: Doesn’t break when class names or IDs change.
Readable: Clearly communicates the intent of the test.
Built-in filtering: Supports options like
name,level, andhidden.
đź§Ş Basic Usage
Here’s a simple example using Playwright Component Testing with React:
import { test, expect } from '@playwright/experimental-ct-react';
import LoginForm from './LoginForm';
test('should submit login form', async ({ mount }) => {
const component = await mount(<LoginForm />);
const usernameInput = component.getByRole('textbox', { name: 'Username' });
const passwordInput = component.getByRole('textbox', { name: 'Password' });
const submitButton = component.getByRole('button', { name: 'Login' });
await usernameInput.fill('gunasekaran');
await passwordInput.fill('securepassword');
await submitButton.click();
await expect(component).toContainText('Welcome, gunasekaran!'
Did you know the name parameter can come from multiple sources, not just visible text?. here are the list of scenarios that name can handle.
- Visible text
<button>Submit</button>
await page.getByRole('button', { name: 'Submit' }).click();
aria-label
<button aria-label="Submit Form"></button>
await page.getByRole('button', { name: 'Submit Form' }).click();
aria-labelledby
<label id="lblUser">Username</label>
<input type="text" aria-labelledby="lblUser">
await page.getByRole('textbox', { name: 'Username' }).fill('admin');
- Alt text for images
<img src="logo.png" alt="Company Logo">
await page.getByRole('img', { name: 'Company Logo' }).click();
| DOM Example | Accessible Name | Playwright Locator | Note |
<button>Submit</button> | "Submit" | page.getByRole('button', { name: 'Submit' }) | Simple button with visible text. |
<button>Submit <label>form</lable> </button> | "Submit form" | page.getByRole('button', { name: 'Submit form' }) | button tag contains nested label tag. both can be concatenated in the name and used for identifying |
<input type="text" aria-labelledby="lblUser"><label id="lblUser">Username</label> | "Username" | page.getByRole('textbox', { name: 'Username' }) | Label references input using aria-labelledby. |
<button aria-label="Save Changes"></button> | "Save Changes" | page.getByRole('button', { name: 'Save Changes' }) | aria-label provides accessible name, visible text not required. |
<img src="logo.png" alt="Company Logo"> | "Company Logo" | page.getByRole('img', { name: 'Company Logo' }) | alt attribute used for accessible name. |
<div role="checkbox" aria-checked="true">Accept Terms</div> | "Accept Terms" | page.getByRole('checkbox', { name: 'Accept Terms', checked: true }) | Checkbox with text inside div, state can be filtered. |
<h1>Welcome User</h1> | "Welcome User" | page.getByRole('heading', { name: 'Welcome User', level: 1 }) | Heading with plain text, no nested tags. |
<h1 name='greeting'>Welcome User</h1> | "Welcome User" |
"greeting" —> wrong | page.getByRole('heading', { name: 'Welcome User' }) | Dont get confused with name attributename attribute ≠accessible name in Playwright. |
There are html elements that has implicit role so it won’t have the role attribute manually but still can be identified using role
âś… Implicit ARIA Roles in Native HTML Elements
| Implicit ARIA Role | HTML Elements |
link | <a href>, <area href> |
button | <button>, <input type="button">, <input type="submit">, <input type="reset">, <input type="image"> |
checkbox | <input type="checkbox"> |
radio | <input type="radio"> |
slider | <input type="range"> |
textbox | <input type="email">, <input type="tel">, <input type="text">, <input type="url">, <textarea> |
listbox | <select> |
option | <option> |
progressbar | <progress> |
text | <b>, <i>, <u>, <strong>, <em>, <dfn>, <abbr>, <span> |
meter | <meter> |
form | <form> |
table | <table> |
columnheader / rowheader | <th> |
cell | <td> |
row | <tr> |
rowgroup | <thead>, <tbody>, <tfoot> |
list | <ul>, <ol> |
listitem | <li> |
dialog | <dialog> |
heading | <h1>, <h2>, <h3>, <h4>, <h5>, <h6> |
document | <iframe>, <code>, <pre>, <cite>, <time> |
img | <img> (if alt attribute is present) |
banner | <header> |
contentinfo | <footer> |
main | <main> |
article | <article> |
complementary | <aside> |
navigation | <nav> |
region | <section>, <blockquote>, <address> |
figure | <figure> |
caption | <figcaption> |
group | <summary>, <details> |
📚 Conclusion
Using getByRole in Playwright helps you write tests that are not only more reliable but also promote better accessibility. It’s a win-win for developers and users alike. Whether you're testing components or full pages, adopting role-based queries will elevate the quality of your test suite.




