Q19 of 24 · Accessibility

What is the difference between ARIA roles, states, and properties, and how do you validate them in tests?

AccessibilitySenioraccessibilityariarolesstatespropertiesplaywrighttesting

Short answer

Short answer: Roles define what an element is (role='dialog', role='checkbox'). States describe its current condition and change dynamically (aria-expanded, aria-checked). Properties describe characteristics that rarely change (aria-label, aria-labelledby, aria-required). Together they give assistive technology a complete semantic description of a widget.

Detail

Roles: define the element's type in the accessibility tree. Roles come from the ARIA specification and map to native HTML elements or extend the semantics for custom widgets. A role tells screen readers what keyboard interactions to expect (a combobox expects arrow key navigation; a button expects Enter/Space activation). Roles are set once and don't change dynamically.

States: describe the current condition of an element — and they must be updated by JavaScript as the condition changes. Common states: aria-expanded (true/false on a toggle button), aria-checked (true/false/mixed on a checkbox), aria-selected (true/false on a tab or option), aria-disabled (true/false), aria-invalid (true/false/grammar/spelling). If a button controls an expandable section and aria-expanded is never updated from "false" to "true" when the section opens, screen reader users hear "collapsed" when the section is actually open.

Properties: structural attributes that don't typically change after render. aria-label (the accessible name), aria-labelledby (references another element's text), aria-describedby (references a description), aria-required, aria-haspopup. These define the widget's relationships and labels.

Validation in tests: use the browser's accessibility API via Playwright's getByRole and assertions:

// Verify state updates correctly
await page.getByRole('button', { name: 'Show filters' }).click();
await expect(page.getByRole('button', { name: 'Show filters' }))
  .toHaveAttribute('aria-expanded', 'true');

// Verify the revealed region
await expect(page.getByRole('region', { name: 'Filters' })).toBeVisible();

Also use axe-core for structural validation — it catches ARIA roles used in invalid contexts (e.g. role="listitem" without a parent role="list").

// WHAT INTERVIEWERS LOOK FOR

Clear distinction between the three — especially that states are dynamic and must be JavaScript-maintained. Demonstrates how to validate state updates in tests.