Q19 of 24 · Accessibility
What is the difference between ARIA roles, states, and properties, and how do you validate them in tests?
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").