skip to content

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

level: middleimportance: should knowfreq 51%

answer

  1. Not every option fits every role
  2. One option is a number, not a boolean
  3. Read from the accessibility view, not CSS
  4. Narrowing the query is not asserting
  5. Unsupported pairing raises a listing error

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.

solid answer

~40 s

They filter the query by **ARIA state**, and each is scoped to particular roles. `pressed` is accepted for `button` only; `checked` for `checkbox`, `radio`, `switch`, `option`, `menuitemcheckbox`, `menuitemradio` and `treeitem`; `selected` for `option`, `tab`, `row`, `gridcell`, `rowheader`, `columnheader` and `treeitem`; `level` for `heading`, `listitem`, `row` and `treeitem`. Pairing one with a role that does not support it raises an error listing the roles that do. The value is computed from the accessibility view — `aria-checked`, `aria-pressed`, `aria-expanded`, `aria-selected`, `aria-level`, or a native control's own state — never from a CSS class. Remember these **narrow the locator**, they do not assert: `{ pressed: true }` on a toggle that is off resolves to nothing and times out, whereas locating by name and asserting on state fails with a message about the state.

code

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

test('board filters and issue sections', async ({ page }) => {
  await page.goto('/board');

  // aria-pressed toggle: click it only while it is off
  await page.getByRole('button', { name: 'Show closed', pressed: false }).click();

  // native input.checked drives the checked option
  await expect(page.getByRole('checkbox', { name: 'Only my issues', checked: true })).toBeVisible();

  // level comes free from <h2>; there are three sections
  await page.goto('/issues/42');
  await expect(page.getByRole('heading', { level: 2 })).toHaveCount(3);
});

go deeper

for a junior

Know that getByRole takes more than a name: options like checked, pressed, expanded, selected and level narrow the query by the element's accessibility state rather than by its appearance.

for a middle

Explain which roles accept which option, that pressed is button-only and level is a number for headings, and where the value comes from in the aria attributes or the native control.

for a senior

Demonstrate the filter-versus-assert judgment. Show that a state option in the locator turns a wrong state into a not-found timeout, and choose deliberately which failure message you want.

for a principal

Own the convention for how components publish state. If toggles signal only through CSS, no locator can express the check, so the ARIA contract has to be part of the component definition of done.

Beyond `name`, `page.getByRole` accepts a set of options that filter on **ARIA state**: `checked`, `pressed`, `expanded`, `selected`, `level` and `disabled`. Each is computed from the accessibility view — an `aria-*` attribute, or the native control's own state — never from a CSS class such as `.is-active`. ## Each option is scoped to the roles that support it The state options are not universal. Playwright validates the combination and raises an error naming the roles that support the attribute, for example `"pressed" attribute is only supported for roles: "button"`. | Option | Roles that accept it | |---|---| | `checked` | `checkbox`, `radio`, `switch`, `option`, `menuitemcheckbox`, `menuitemradio`, `treeitem` | | `pressed` | `button` only | | `selected` | `option`, `tab`, `row`, `gridcell`, `rowheader`, `columnheader`, `treeitem` | | `expanded` | widget roles including `button`, `combobox`, `link`, `row`, `tab`, `treeitem`, `menuitem` | | `level` | `heading`, `listitem`, `row`, `treeitem` | `disabled` is the exception: it applies to any role, and it is inherited down the DOM, so a control inside a container marked `aria-disabled="true"` counts as disabled too. ## Where the state value comes from - `checked` reads the native `checked` property of `<input type="checkbox">` and `<input type="radio">`, and `aria-checked` for anything using an ARIA role. - `pressed` reads `aria-pressed`, the toggle-button pattern — an issue board's `Show closed` toggle is the canonical case. - `expanded` reads `aria-expanded`, which is how a disclosure control announces whether its panel is open. - `selected` reads `aria-selected`; note that a `<select>`'s chosen `<option>` is a different mechanism, and `selected` here is about ARIA state. - `level` reads the native heading rank for `<h1>`–`<h6>`, and `aria-level` for roles that support it. ## Filtering versus asserting These options change **which element the locator resolves to**, they do not check anything. That distinction matters: 1. `page.getByRole('button', { name: 'Show closed', pressed: true })` resolves to nothing while the toggle is off, so an action on it times out with a "not found" style failure. 2. Locating the button by name only, then asserting on its state, produces a failure message that says the state was wrong. Use the state option when the state is part of *identifying* the element — one of several tabs, the checked radio in a group — and locate without it when the state is the thing under test. ## `level` and heading structure `level` is the odd one out because it is a number, not a boolean. On an issue detail page whose sections are `<h2>` and whose issue title is `<h1>`, `page.getByRole('heading', { level: 2 })` selects the section headings and skips the title. Because levels 1–6 come free from the tag name, this is a cheap way to assert document structure without touching class names. A `role="heading"` element with no `aria-level` has no usable level, so pair the explicit role with an explicit `aria-level` when hand-rolling headings. ## Worked example on the tracker ```ts // Board filters await page.getByRole('button', { name: 'Show closed', pressed: false }).click(); await expect(page.getByRole('checkbox', { name: 'Only my issues', checked: true })).toBeVisible(); // Issue detail await page.getByRole('tab', { name: 'Comments', selected: true }).click(); await expect(page.getByRole('heading', { level: 2 })).toHaveCount(3); ``` ## Common mistakes - Reaching for `pressed` on a `<div role="tab">` or `checked` on a `link` — both are rejected with an error naming the supported roles. - Expecting `checked: false` to match an element that has no checked state at all; it matches controls that *have* the state and are unchecked, not everything else on the page. - Using a state option in place of an assertion and then reading the resulting timeout as "the state never changed", when the real cause may be a name mismatch. - Assuming a visually pressed button is `pressed: true`; if the component signals state with a CSS class and no `aria-pressed`, the accessibility view says nothing and the option cannot help.

  • Why might a visually pressed toggle not match pressed: true in Playwright?
    Because the option reads `aria-pressed`, not appearance. If the component signals its state with a CSS class such as `.is-active` and never sets `aria-pressed`, the accessibility view reports no pressed state at all, so neither `true` nor `false` matches. That gap is an accessibility bug, and the test failure is the first place it surfaces.
  • When should state be part of the locator rather than an assertion?
    Put it in the locator when the state identifies the element, for instance the currently selected tab among several, or the checked radio in a group. Keep it out when the state is the thing under test, because a locator that fails to resolve reports a not-found timeout rather than telling you the state was wrong.
  • How does the level option behave for a hand-rolled heading?
    For `<h1>`–`<h6>` the level 1 to 6 comes free from the tag name. An element with `role="heading"` needs an explicit `aria-level` for the option to match, otherwise it has no usable level. That is one more reason to prefer real heading tags over a role attribute on a div.

saying these in an interview costs you the question

  • Thinks every state option works with every role
  • Believes the options read CSS classes such as is-active
  • Uses a state option instead of an assertion, then misreads the timeout
  • Thinks checked: false matches every element without a checkbox
  • Assumes level applies to any element with large text