API Reference August 17, 2026 15 min read

Playwright getByRole Guide: Every Role, Option & Example (2026)

The Playwright team recommends getByRole as your default locator strategy. This guide is the complete reference — every ARIA role, every filtering option, and production-ready code examples you can copy into your tests today.

getByRole queries the browser's accessibility tree instead of the raw DOM. That single difference makes it the most resilient, most readable, and most accessibility-friendly locator strategy in Playwright. If your tests use getByRole as the default, they simultaneously verify that your app works and that it is accessible to screen readers and assistive technologies.

This guide covers every role, every option, and every pattern you need. Whether you are new to Playwright or migrating an existing test suite away from CSS selectors, you will find complete, copy-paste-ready examples for every scenario. For a broader comparison of all locator types, see the Playwright Locators Guide.


Why getByRole Is the Recommended Locator

The Playwright documentation explicitly states: "Role locators are the recommended way to locate elements." There are three reasons this advice is worth following.

1. Accessibility-first testing. When you write page.getByRole('button', { name: 'Submit' }), you are asserting that a button element exists with the accessible name "Submit." If a developer replaces the <button> with a styled <div> that has no ARIA role, the test fails — catching an accessibility regression before it reaches production.

2. Resilient to refactors. CSS class names, data attributes, and DOM structure change constantly during development. ARIA roles and accessible names rarely change because they are tied to what the element does, not how it is styled. A locator like getByRole('link', { name: 'Pricing' }) survives a complete CSS rewrite, a component library migration, or a framework switch.

3. Self-documenting tests. Compare page.locator('.btn-primary.cta-hero') with page.getByRole('button', { name: 'Enroll Now' }). The second version tells you exactly what the test is looking for without opening the source code. New team members understand the test intent immediately.

Tip: Playwright's code generator (npx playwright codegen) already prefers getByRole when recording actions. If you record a click on a button, the generated code will use getByRole('button', { name: '...' }) by default.


How getByRole Works

Every HTML element has an ARIA role — either implicit (assigned by the browser based on the tag) or explicit (set via the role attribute). The browser builds an accessibility tree from these roles, and getByRole queries that tree directly.

Implicit vs. Explicit Roles

Most standard HTML elements have implicit roles defined by the HTML specification. You do not need to add any attributes for these to work:

Implicit roles — HTML elements and their built-in ARIA roles
<!-- role="button" (implicit) -->
<button>Save</button>

<!-- role="link" (implicit, only when href is present) -->
<a href="/pricing">Pricing</a>

<!-- role="textbox" (implicit) -->
<input type="text" />

<!-- role="checkbox" (implicit) -->
<input type="checkbox" />

<!-- role="heading" with level 2 (implicit) -->
<h2>Features</h2>

<!-- role="navigation" (implicit) -->
<nav>...</nav>

When the implicit role is not enough — for example, a custom dropdown built with <div> elements — you assign an explicit role:

Explicit role on a custom component
<!-- Custom dropdown with explicit combobox role -->
<div role="combobox" aria-expanded="false" aria-label="Select country">
  Select a country...
</div>

The Accessibility Tree

The accessibility tree is a parallel representation of the DOM that strips away visual details and keeps only semantic information: roles, names, states, and relationships. When you call page.getByRole('button'), Playwright walks this tree — not the DOM — to find matching nodes. This means elements hidden from the accessibility tree (via aria-hidden="true" or display: none) are excluded by default.

Key insight: If getByRole cannot find your element, it usually means the element is not properly represented in the accessibility tree. This is a signal that your HTML semantics need fixing — not that you should switch to a CSS selector.


The name Option

Most pages have multiple buttons, multiple links, and multiple headings. The name option filters by accessible name — the text that screen readers announce for the element. The accessible name is computed from (in priority order):

  1. aria-labelledby (references another element's text)
  2. aria-label (direct string label)
  3. Associated <label> element (for form controls)
  4. Element's text content (for buttons, links, headings)
  5. alt attribute (for images)
  6. title attribute (fallback)

Exact string match

Exact name matching
// Matches <button>Submit</button>
page.getByRole('button', { name: 'Submit' });

// Matches <a href="/pricing">Pricing</a>
page.getByRole('link', { name: 'Pricing' });

By default, name matching is case-insensitive and matches a substring. So { name: 'submit' } will match a button with text "Submit Form".

Exact match with exact: true

Exact matching prevents substring matches
// Will NOT match "Submit Form" — only exact "Submit"
page.getByRole('button', { name: 'Submit', exact: true });

Regex matching

Regex for flexible name patterns
// Match any button containing "delete" (case-insensitive)
page.getByRole('button', { name: /delete/i });

// Match a link that starts with "Read more"
page.getByRole('link', { name: /^Read more/ });

// Match heading with dynamic user name
page.getByRole('heading', { name: /Welcome, .+/ });

Tip: When a button has no visible text but uses an icon, it should have an aria-label. That label becomes the accessible name: <button aria-label="Close"><svg>...</svg></button> is matched by getByRole('button', { name: 'Close' }).


Common Roles with Examples

Below is a practical reference for the roles you will use most often. Each example shows the HTML element and the corresponding getByRole call.

button

Matches <button>, <input type="submit">, <input type="button">, and any element with role="button".

HTML
<button>Save Changes</button>
<input type="submit" value="Submit Form" />
<button aria-label="Close dialog">
  <svg>...</svg>
</button>
Playwright locators
// Text button
page.getByRole('button', { name: 'Save Changes' });

// Submit input
page.getByRole('button', { name: 'Submit Form' });

// Icon button with aria-label
page.getByRole('button', { name: 'Close dialog' });

Matches <a href="..."> elements. Note: an <a> without href does not have an implicit link role.

HTML
<a href="/pricing">Pricing</a>
<a href="/docs" aria-label="Documentation">
  <svg>...</svg> Docs
</a>
Playwright locators
page.getByRole('link', { name: 'Pricing' });
page.getByRole('link', { name: 'Documentation' });

textbox

Matches <input type="text">, <input type="email">, <input type="password">, <input> (with no type), and <textarea>. The accessible name comes from the associated <label> or aria-label.

HTML
<label for="email">Email address</label>
<input type="email" id="email" />

<textarea aria-label="Your message"></textarea>
Playwright locators
page.getByRole('textbox', { name: 'Email address' });
page.getByRole('textbox', { name: 'Your message' });

checkbox and radio

Matches <input type="checkbox"> and <input type="radio">. Use the checked option to filter by state.

HTML
<label>
  <input type="checkbox" /> I agree to the terms
</label>

<label>
  <input type="radio" name="plan" /> Monthly
</label>
<label>
  <input type="radio" name="plan" /> Annual
</label>
Playwright locators
// Find the checkbox
page.getByRole('checkbox', { name: 'I agree to the terms' });

// Find only checked checkboxes
page.getByRole('checkbox', { checked: true });

// Find a specific radio button
page.getByRole('radio', { name: 'Annual' });

combobox

Matches <select> elements and custom dropdowns with role="combobox". Note that <select> with no multiple attribute has the implicit role combobox, while <select multiple> has role listbox.

HTML
<label for="country">Country</label>
<select id="country">
  <option value="us">United States</option>
  <option value="uk">United Kingdom</option>
</select>
Playwright locator
page.getByRole('combobox', { name: 'Country' });

heading (h1–h6 with level option)

All heading elements (<h1> through <h6>) share the role heading. Use the level option to target a specific depth.

HTML
<h1>Dashboard</h1>
<h2>Recent Activity</h2>
<h3>Revenue Chart</h3>
Playwright locators
// Any heading with text "Dashboard"
page.getByRole('heading', { name: 'Dashboard' });

// Only the h1
page.getByRole('heading', { name: 'Dashboard', level: 1 });

// Any h2 on the page
page.getByRole('heading', { level: 2 });

dialog

Matches <dialog> elements and any element with role="dialog" or role="alertdialog".

HTML
<dialog open aria-label="Confirm deletion">
  <p>Are you sure you want to delete this item?</p>
  <button>Cancel</button>
  <button>Delete</button>
</dialog>
Playwright locator
// Find the dialog by its accessible name
const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });

// Then interact with elements inside it
await dialog.getByRole('button', { name: 'Delete' }).click();

navigation, main, banner, contentinfo (landmarks)

HTML5 landmark elements have implicit roles that let you scope locators to specific page regions. This is extremely useful when the same button text appears in multiple areas of the page.

HTML landmark elements and their implicit roles
<header>...</header>        <!-- role="banner" -->
<nav aria-label="Main">...</nav>  <!-- role="navigation" -->
<main>...</main>            <!-- role="main" -->
<footer>...</footer>        <!-- role="contentinfo" -->
<aside>...</aside>          <!-- role="complementary" -->
Scoping locators to landmarks
// Click "Home" link inside the main navigation only
await page.getByRole('navigation', { name: 'Main' })
  .getByRole('link', { name: 'Home' })
  .click();

// Find the search button in the header, not the footer
await page.getByRole('banner')
  .getByRole('button', { name: 'Search' })
  .click();

// Assert footer contains copyright
await expect(page.getByRole('contentinfo'))
  .toContainText('2026');

row, cell, columnheader (tables)

HTML tables have rich implicit roles: <table> is table, <tr> is row, <td> is cell, and <th> is columnheader (or rowheader).

HTML
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alice</td>
      <td>alice@test.com</td>
      <td>Active</td>
    </tr>
  </tbody>
</table>
Playwright locators
// Find the "Name" column header
page.getByRole('columnheader', { name: 'Name' });

// Find a row containing "Alice"
page.getByRole('row', { name: 'Alice' });

// Find a cell with specific content
page.getByRole('cell', { name: 'Active' });

// Count all data rows
await expect(page.getByRole('row')).toHaveCount(2); // header + 1 data row

tab and tabpanel

Tab interfaces use role="tab" for the tab buttons and role="tabpanel" for the content panels. Use the selected option to find the active tab.

HTML
<div role="tablist">
  <button role="tab" aria-selected="true">General</button>
  <button role="tab" aria-selected="false">Security</button>
  <button role="tab" aria-selected="false">Billing</button>
</div>
<div role="tabpanel">General settings...</div>
Playwright locators
// Click the Security tab
await page.getByRole('tab', { name: 'Security' }).click();

// Find the currently selected tab
page.getByRole('tab', { selected: true });

// Assert the visible tab panel has content
await expect(page.getByRole('tabpanel'))
  .toContainText('Two-factor authentication');

menuitem and menu

Matches dropdown menus and context menus built with role="menu" and role="menuitem".

HTML
<div role="menu" aria-label="File">
  <button role="menuitem">New</button>
  <button role="menuitem">Open</button>
  <button role="menuitem">Save</button>
</div>
Playwright locators
// Click "Save" in the File menu
await page.getByRole('menu', { name: 'File' })
  .getByRole('menuitem', { name: 'Save' })
  .click();

list and listitem

Matches <ul>/<ol> (role list) and <li> (role listitem).

HTML
<ul aria-label="Shopping cart">
  <li>Keyboard - $79</li>
  <li>Mouse - $49</li>
</ul>
Playwright locators
// Find the shopping cart list
page.getByRole('list', { name: 'Shopping cart' });

// Count items in the cart
await expect(
  page.getByRole('list', { name: 'Shopping cart' })
    .getByRole('listitem')
).toHaveCount(2);

Options Deep Dive

getByRole accepts a second argument with filtering options. Here is every option and when to use it:

All getByRole options
page.getByRole(role, {
  name: '...',          // Filter by accessible name (string or RegExp)
  exact: true,         // Exact name match (case-sensitive, no substring)
  checked: true,       // checkbox/radio: only checked elements
  disabled: true,      // Only disabled elements
  expanded: true,      // Only expanded elements (aria-expanded="true")
  includeHidden: true, // Include elements hidden from accessibility tree
  level: 2,            // heading level (1-6)
  pressed: true,       // Toggle button: only pressed (aria-pressed="true")
  selected: true,      // tab/option: only selected (aria-selected="true")
});

checked

Filters checkboxes and radio buttons by their checked state. Pass false to find unchecked elements.

checked option
// Find all checked checkboxes
page.getByRole('checkbox', { checked: true });

// Find all unchecked checkboxes
page.getByRole('checkbox', { checked: false });

disabled

Filters by the disabled attribute or aria-disabled.

disabled option
// Find the disabled submit button
page.getByRole('button', { name: 'Submit', disabled: true });

expanded

Filters by aria-expanded. Useful for accordions, collapsible menus, and disclosure widgets.

expanded option
// Find the expanded accordion item
page.getByRole('button', { expanded: true });

// Find a collapsed dropdown trigger
page.getByRole('button', { name: 'Options', expanded: false });

includeHidden

By default, getByRole skips elements hidden from the accessibility tree. Pass includeHidden: true to override this and find hidden elements.

includeHidden option
// Find a hidden modal that has not been opened yet
page.getByRole('dialog', {
  name: 'Settings',
  includeHidden: true,
});

pressed

Filters toggle buttons by their pressed state (aria-pressed).

pressed option
// Find the "Bold" button when it is active
page.getByRole('button', { name: 'Bold', pressed: true });

selected

Filters tabs and listbox options by their selected state (aria-selected).

selected option
// Find the currently active tab
page.getByRole('tab', { selected: true });

// Assert which tab is selected
await expect(page.getByRole('tab', { selected: true }))
  .toHaveText('General');

getByRole vs Other Locators

Playwright offers several locator strategies. Here is when to use each one:

getByRole — Your default choice. Use for any element that has a meaningful ARIA role (buttons, links, inputs, headings, landmarks, tables, dialogs). Validates accessibility and survives refactors.

getByLabel — Best for form inputs when you want to target the <label> text directly. Internally similar to getByRole('textbox', { name: 'Email' }), but also works for elements where the role is ambiguous.

getByText — Use for non-interactive elements (paragraphs, spans, divs) that have no semantic role. Avoid for buttons and links — prefer getByRole for those.

getByTestId — The escape hatch. Use when an element has no accessible role, no label, and no visible text. Common for containers, wrappers, and generated components. Does not validate accessibility.

CSS / XPath selectors — Last resort. Brittle and implementation-dependent. If you find yourself reaching for page.locator('.btn-primary'), ask whether the element should have a proper ARIA role first.

Prefer this
page.getByRole('button', { name: 'Save' })
Avoid this
page.locator('.btn-primary.save-btn')
Prefer this
page.getByRole('link', { name: 'Pricing' })
Avoid this
page.locator('a[href="/pricing"]')

Tip: For a complete comparison including performance considerations and chaining patterns, see the Playwright Locators Guide.


Real-World Patterns

These patterns show how getByRole is used in complete test scenarios, not just isolated locator calls.

Login form

Complete login form test
import { test, expect } from '@playwright/test';

test('successful login redirects to dashboard', async ({ page }) => {
  await page.goto('https://example.com/login');

  // Fill the form using role locators
  await page.getByRole('textbox', { name: 'Email' })
    .fill('user@example.com');
  await page.getByRole('textbox', { name: 'Password' })
    .fill('SecurePass123!');

  // Submit
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Assert redirect
  await expect(page).toHaveURL(/\/dashboard/);
  await expect(page.getByRole('heading', { level: 1 }))
    .toHaveText('Dashboard');
});

Navigation menu

Navigation test scoped to landmark
test('main navigation has correct links', async ({ page }) => {
  await page.goto('https://example.com');

  const nav = page.getByRole('navigation', { name: 'Main' });

  // Verify link count
  await expect(nav.getByRole('link')).toHaveCount(5);

  // Verify link text in order
  await expect(nav.getByRole('link'))
    .toHaveText(['Home', 'Products', 'Pricing', 'Docs', 'Contact']);

  // Click and verify navigation
  await nav.getByRole('link', { name: 'Pricing' }).click();
  await expect(page).toHaveURL(/\/pricing/);
});

Data table

Data table interaction and verification
test('table filtering works', async ({ page }) => {
  await page.goto('https://example.com/users');

  // Verify column headers
  await expect(page.getByRole('columnheader'))
    .toHaveText(['Name', 'Email', 'Role', 'Status']);

  // Filter the table
  await page.getByRole('textbox', { name: 'Search users' })
    .fill('admin');

  // Verify filtered results (header row + data rows)
  await expect(page.getByRole('row')).toHaveCount(3);

  // Verify a specific cell in the first data row
  const firstRow = page.getByRole('row').nth(1);
  await expect(firstRow.getByRole('cell').first())
    .toContainText('admin');
});

Modal dialog

Modal open, interact, close
test('delete confirmation dialog works', async ({ page }) => {
  await page.goto('https://example.com/settings');

  // Click delete to open confirmation dialog
  await page.getByRole('button', { name: 'Delete account' }).click();

  // Wait for dialog to appear
  const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });
  await expect(dialog).toBeVisible();

  // Verify dialog content
  await expect(dialog)
    .toContainText('This action cannot be undone');

  // Cancel the deletion
  await dialog.getByRole('button', { name: 'Cancel' }).click();

  // Verify dialog closed
  await expect(dialog).not.toBeVisible();
});

Accordion / FAQ

Accordion expand/collapse test
test('FAQ accordion expands and collapses', async ({ page }) => {
  await page.goto('https://example.com/faq');

  const question = page.getByRole('button', {
    name: 'What is your refund policy?',
  });

  // Initially collapsed
  await expect(question).toHaveAttribute('aria-expanded', 'false');

  // Click to expand
  await question.click();
  await expect(question).toHaveAttribute('aria-expanded', 'true');

  // Alternatively, use the expanded option
  await expect(page.getByRole('button', {
    name: 'What is your refund policy?',
    expanded: true,
  })).toBeVisible();
});

Debugging getByRole

When getByRole cannot find your element, use these techniques to diagnose the problem.

highlight() for visual debugging

The highlight() method draws a red border around all matched elements in the browser. This is useful when you are not sure which elements your locator is finding.

Visual highlighting in headed mode
// Highlight all buttons on the page
await page.getByRole('button').highlight();

// Highlight a specific element
await page.getByRole('link', { name: 'Pricing' }).highlight();

Playwright Inspector

Run your test with --debug to open the Playwright Inspector. It shows the accessibility tree for the entire page and lets you test locators interactively:

Terminal
npx playwright test --debug

In the Inspector's "Locator" tab, type a getByRole expression and it highlights matching elements in real time.

Accessibility tree snapshot

You can dump the entire accessibility tree to the console. This reveals exactly what roles and names are available on the page:

Print the accessibility tree
const snapshot = await page.accessibility.snapshot();
console.log(JSON.stringify(snapshot, null, 2));

The output shows every node with its role, name, value, and children. Search the output for the element you are trying to find — if it is missing or has a different role/name than expected, that explains why your locator fails.

Tip: Chrome DevTools also shows the accessibility tree. Open DevTools, go to Elements, and look for the "Accessibility" pane. This is useful for investigating roles without running a Playwright test.


Common Mistakes & Fixes

Mistake 1: Using the wrong role

A common error is using getByRole('input') — but there is no ARIA role called "input." The correct role for text inputs is textbox, for checkboxes it is checkbox, and for dropdowns it is combobox.

Do this
page.getByRole('textbox', { name: 'Email' })
Not this
page.getByRole('input', { name: 'Email' })

Mistake 2: Missing accessible name

If your <input> has no <label>, no aria-label, and no aria-labelledby, then its accessible name is empty. The locator getByRole('textbox', { name: 'Email' }) will not find it.

Do this
<label for="email">Email</label> <input id="email" />
Not this
<input placeholder="Email" />

The placeholder attribute does not count as an accessible name in most browsers. Always use a <label> or aria-label.

Mistake 3: Forgetting that <a> without href has no link role

An <a> element only has the implicit link role when it has an href attribute. Without href, it is treated as generic text.

Has link role
<a href="/pricing">Pricing</a>
No link role
<a>Pricing</a>

Mistake 4: Trying to find hidden elements

Elements with display: none, visibility: hidden, or aria-hidden="true" are excluded from the accessibility tree by default. If you need to find them, use includeHidden: true.

Finding hidden elements
// This will NOT find a hidden dialog
page.getByRole('dialog'); // fails if dialog is display:none

// This will find it
page.getByRole('dialog', { includeHidden: true });

Mistake 5: Confusing implicit roles for <div> and <span>

<div> and <span> have no implicit ARIA role. If your component is built entirely with divs, getByRole will not find anything unless you add explicit role attributes. This is actually a feature — it reveals inaccessible markup.


Generate getByRole Locators with Claude AI

Writing getByRole locators for every element on a complex page is time-consuming. In the Playwright + Claude AI & MCP Server course, you will learn how to use Claude AI to automatically analyze your page's accessibility tree and generate the optimal getByRole calls. Claude identifies the correct role, computes the accessible name, and suggests the right filtering options — so you get production-ready locators in seconds instead of minutes.

The course covers how to connect Claude AI to your Playwright project via the MCP Server, enabling a workflow where you describe what you want to test and AI writes the locators for you.


Frequently Asked Questions

What is the difference between getByRole and getByTestId in Playwright?

getByRole queries the accessibility tree using ARIA roles and accessible names, mimicking how assistive technologies see the page. getByTestId queries a custom data attribute (data-testid by default). Playwright recommends getByRole as the primary locator because it validates accessibility and is resilient to markup changes. Use getByTestId as a fallback when an element has no meaningful ARIA role or accessible name.

How does Playwright determine the role of an HTML element?

Playwright uses the same ARIA role resolution as browsers. Most HTML elements have implicit roles defined by the HTML spec — for example, <button> has role button, <a href> has role link, and <input type="checkbox"> has role checkbox. You can also assign explicit roles with the role attribute. Playwright queries the computed accessibility tree, so both implicit and explicit roles work.

What does the name option do in getByRole?

The name option filters elements by their accessible name. The accessible name is computed from the element's text content, aria-label, aria-labelledby, associated <label>, alt text (for images), or title attribute. For example, getByRole('button', { name: 'Submit' }) finds a button whose accessible name is "Submit." You can pass a string for substring match or a regex for pattern matching.

Can getByRole find elements that are hidden with display:none?

By default, getByRole only matches elements visible in the accessibility tree. Elements hidden with display:none, visibility:hidden, or aria-hidden="true" are excluded. To locate a hidden element, pass the includeHidden: true option: page.getByRole('button', { name: 'Submit', includeHidden: true }).

How do I use getByRole with heading levels like h1, h2, h3?

All heading elements (<h1> through <h6>) share the ARIA role heading. To target a specific level, use the level option: page.getByRole('heading', { level: 1 }) matches only <h1> elements. You can combine level with name to be even more specific: page.getByRole('heading', { name: 'Dashboard', level: 2 }).


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn