skip to content

Role and Name

getByRole matches an element's ARIA role together with its accessible name, with options for level, checked state and hidden elements. Asked because it is the built-in default.

on this pageshow

explore

questions

5

In Playwright, what does page.getByRole('heading', { name: 'Issue 42' }) actually match?

level: juniorimportance: must knowfreq 86%

answer

  1. Two facts must match, not one
  2. Where the role comes from
  3. Tag mapping versus role attribute
  4. Announced name, not inner text
  5. Substring and case-insensitive by default

basics

~20 s

page.getByRole('heading', { name: 'Issue 42' }) matches an element whose ARIA role is heading, implicit for h1 to h6 or set by a role attribute, and whose computed accessible name contains the text Issue 42.

solid answer

~40 s

It queries the accessibility view, not the markup. The first argument is the ARIA role: Playwright takes an explicit `role` attribute if present, otherwise an implicit mapping from the tag, so `<h1>`–`<h6>` are `heading`, `<button>` is `button`, `<a href>` is `link`. The `name` option matches the **accessible name** — the string a screen reader announces — computed from `aria-labelledby`, then `aria-label`, then a native alternative such as a `<label>`, `alt` or `title`, and only then the element's own text for roles that allow a name from content. Matching is a case-insensitive substring by default, with whitespace normalised on both sides. Both facets must hold, which is what makes the pair specific enough to identify one element on a busy page. A plain `<div>` has no role, so `getByRole` cannot reach it.

code

typescript · 14 lines
typescript
import { test, expect } from '@playwright/test';

test('issue detail exposes role and name', async ({ page }) => {
  await page.goto('/issues/42');

  // <h1>Issue 42</h1> -> role heading, name "Issue 42"
  await expect(page.getByRole('heading', { name: 'Issue 42' })).toBeVisible();

  // <button aria-label="Add comment"><svg/></button> -> name from aria-label
  await page.getByRole('button', { name: 'Add comment' }).click();

  // <a href="/board">Back to board</a> -> role link, name from content
  await page.getByRole('link', { name: 'Back to board' }).click();
});

go deeper

for a junior

Remember it takes two things: a role such as button, heading or link, and the name option matching the label a user hears. Know that h1 to h6 are headings and that a plain div has no role.

for a middle

Be able to explain where the role comes from, explicit attribute versus implicit tag mapping, and walk the accessible-name precedence from aria-labelledby down to text content without notes.

for a senior

Show how you diagnose a miss: relax the query to role alone, count the matches, then reintroduce the name. Treat an element with no role or no name as a product defect, not a test problem.

for a principal

Own the position that role and name locators are only as good as the accessibility contract the components publish. Decide where that contract is enforced so tests and assistive technology read the same page.

`page.getByRole(role, options)` builds a locator from two independent facts about an element: the **ARIA role** it exposes to assistive technology, and the **accessible name** computed for it. Both are derived from the rendered accessibility view of the page, not from the CSS classes or the DOM shape you happened to write. ## The role argument The first argument is a role name from the ARIA taxonomy — `button`, `heading`, `link`, `checkbox`, `dialog`, `row`, `tab`, `textbox` and so on. In TypeScript the parameter is typed as a union of the roles Playwright supports, so a typo such as `'headding'` is a compile error rather than a locator that silently matches nothing. Playwright computes the role the way a browser's accessibility layer does: - An explicit `role` attribute wins, as long as its value is a role Playwright recognises. - Otherwise an implicit mapping from the tag name applies: `<button>` is `button`, `<h1>`–`<h6>` are `heading`, `<a href="...">` is `link`, `<input type="checkbox">` is `checkbox`, `<ul>` is `list`, `<li>` is `listitem`, `<table>` is `table`, `<tr>` is `row`. - Several mappings are conditional. `<a>` **without** an `href` has no role at all, `<img alt="">` maps to `presentation` instead of `img`, and `<section>` only becomes `region` when it carries an accessible name. - A bare `<div>` or `<span>` has no implicit role, so no `getByRole` call can reach it until the markup gives it one. ## The name option `{ name: 'Issue 42' }` matches the element's **accessible name** — the single string assistive technology announces for it. That string is computed rather than read off the element, and it is only sometimes the same as the visible text. Simplified, the precedence is: 1. `aria-labelledby`, resolved by following each id reference and joining the referenced text. 2. `aria-label` on the element itself. 3. A host-language alternative: the associated `<label>` of a form control, `alt` on an image, or the `title` attribute. 4. The element's own content — but only for roles that allow a name from content, such as `button`, `link`, `heading`, `option`, `tab` and `cell`. Whitespace in the computed name is always normalised: leading and trailing space is trimmed and internal runs collapse to a single space, so markup that wraps a label across three source lines still yields `Create issue`. ## Reading real issue-tracker markup | Markup | Role | Accessible name | |---|---|---| | `<button>Create issue</button>` | `button` | `Create issue` | | `<button aria-label="Close"><svg/></button>` | `button` | `Close` | | `<h3>Issue 42</h3>` | `heading` | `Issue 42` | | `<a href="/board">Board</a>` | `link` | `Board` | | `<a>Board</a>` | none | not reachable by role | | `<div class="btn">Create issue</div>` | none | not reachable by role | The last two rows are the point of the whole mechanism: the query is over the accessibility view, so an element that exposes nothing there is invisible to `getByRole` no matter how it looks on screen. ## Why pass both Role alone is almost always ambiguous — an issue board has a dozen buttons — and a name alone loses the distinction between a link, a heading and a button that all read `Board`. Passing both narrows the query to one element while still describing it the way a person perceives it. - Omitting `name` is fine when the role occurs once on the page, for example `page.getByRole('dialog')` for a modal. - Passing `name` matches on a case-insensitive substring by default, so `{ name: 'Issue' }` also matches `Issue 42` and `Issue history`. - `name` accepts a `RegExp` as well as a string when the label is dynamic, for example `{ name: /^Issue \d+$/ }`. ## Verifying what a page exposes When a `getByRole` locator does not match, the fastest check is to inspect the accessibility view rather than to guess. Playwright's codegen and the trace viewer's locator picker both suggest role-and-name locators, which is an easy way to see the name Playwright computed for an element. ```ts // Issue detail page of the tracker await expect(page.getByRole('heading', { name: 'Issue 42' })).toBeVisible(); await page.getByRole('button', { name: 'Add comment' }).click(); await page.getByRole('link', { name: 'Back to board' }).click(); ``` ## What it does not do - It is not an accessibility audit. Matching by role proves the element exposes that role, not that the page is conformant. - It does not read CSS. A `<div class="button">` is not a `button`, and `role="button"` on a `<div>` does not make it focusable or keyboard-operable. - It does not guarantee a single match. If two elements share a role and name, the locator resolves to both, and Playwright refuses to act on an ambiguous locator.

  • Where does the accessible name of an icon-only button come from if it contains no text?
    From an explicit label. Playwright follows `aria-labelledby` first, then `aria-label`, then a host-language alternative such as the `title` attribute or an `<img alt>` inside the button. If none of those exist the button's accessible name is empty, so no `name` value can match it — which is also an accessibility defect worth reporting.
  • Does adding role="button" to a div make it behave like a button in a test?
    It makes `getByRole('button')` find it, and nothing more. The element still is not focusable, does not respond to Enter or Space, and does not fire a click from the keyboard unless the component adds `tabindex` and key handling. A test that passes against a role attribute alone asserts something a keyboard user cannot actually do.

It is like finding someone by job title plus name badge rather than by where they happen to be standing in the room.

saying these in an interview costs you the question

  • Thinks getByRole matches a CSS class or tag name
  • Says the name option matches the id attribute
  • Assumes every element has a role, including plain divs
  • Believes the accessible name is always the inner text
  • Thinks the name must be spelled with exact casing
open as a page

In Playwright, why does page.getByRole('button', { name: 'Save' }) also match a button named 'Save and close'?

level: middleimportance: must knowfreq 74%

basics

~10 s

The name option is a case-insensitive substring test against the accessible name by default, so any name containing Save matches. Pass exact: true for a case-sensitive whole-string match, or a RegExp for full control.

open as a page

In Playwright's page.getByRole, when can you use the checked, pressed, expanded, selected and level options?

level: middleimportance: should knowfreq 51%

basics

~20 s

Only on roles that support the state. Playwright reads the value from an aria attribute or the native control and rejects an unsupported pairing, such as pressed on anything but button, with an error naming the allowed roles.

open as a page

Why does page.getByRole('button', { name: 'Delete issue' }) find nothing in Playwright when that button is in the DOM?

level: seniorimportance: should knowfreq 57%

basics

~10 s

Most often the element is hidden for accessibility purposes. Role queries skip anything with aria-hidden, display none, visibility hidden or the hidden attribute, on itself or an ancestor, unless includeHidden is true.

open as a page

In Playwright, why can page.getByRole reach no element for a clickable div in an issue board's row menu?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A div exposes no ARIA role, so no role query can select it. Playwright derives roles from an explicit role attribute or an implicit tag mapping, and div, span and an anchor without href have none.

open as a page