Code of the Day
IntermediateAccessibility

ARIA Roles and Attributes

ARIA adds semantic meaning to elements that HTML alone can't express — but the first rule of ARIA is to not use it if native HTML already does the job.

Web FoundationsIntermediate8 min read
Recommended first
By the end of this lesson you will be able to:
  • Explain what ARIA is and what problem it solves
  • State the first rule of ARIA and apply it to a design decision
  • Use aria-label, aria-labelledby, and aria-describedby correctly
  • Keep aria-expanded and aria-hidden in sync with visual state
  • Explain what a live region is and when to use one

HTML elements carry built-in semantics — a <button> is announced as a button, a <nav> is announced as a navigation landmark, an <input type="checkbox"> is announced with its checked state. But the web has components that HTML alone cannot express: a custom tab panel, a combobox built from <div> elements, a live notification region. This is the problem ARIA was designed to solve.

— Accessible Rich Internet Applications — is a set of HTML attributes, published by the W3C, that supplements the native semantics of HTML. It communicates three kinds of information to assistive technologies: roles (what something is), properties (what it does), and states (what condition it is currently in).

The first rule of ARIA

Before reaching for any ARIA attribute, apply this rule:

Do not use ARIA if a native HTML element or attribute already provides the semantics and behaviour you need.

This is not unofficial advice — it is the opening line of the W3C's ARIA Authoring Practices Guide. A <button> is always preferable to <div role="button">. A <label> is always preferable to aria-label when visible text is available. Native elements are focusable, keyboard-operable, and announce their semantics correctly out of the box. ARIA only provides the announcement — it does not add focus behaviour or keyboard event handling.

When the native element exists, use it. ARIA fills the gaps.

Roles

A role attribute overrides or supplements the element's native role in the accessibility tree. Some roles you will encounter:

<!-- A non-native dialog — no <dialog> element used -->
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
  <h2 id="dialog-title">Confirm deletion</h2>

</div>

<!-- A status message announced without user navigation -->
<div role="alert">Your changes have been saved.</div>

<!-- A tab strip built from custom elements -->
<div role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1">Overview</button>
  <button role="tab" aria-selected="false" aria-controls="panel-2">Details</button>
</div>
<div role="tabpanel" id="panel-1">…</div>
<div role="tabpanel" id="panel-2" hidden>…</div>

The native <dialog> element now has broad browser support and carries role="dialog" implicitly. Prefer it over a <div role="dialog"> — it handles focus management and the Escape key for free.

Properties: labelling and describing

Properties are stable facts about an element. The three you will use most:

aria-label — provides an accessible name when there is no visible label text. Use it on icon-only buttons:

<!-- The "X" is visual shorthand — the accessible name should be descriptive -->
<button aria-label="Close dialog">✕</button>

aria-labelledby — points to an existing element whose text becomes the accessible name. Preferred over aria-label when a visible label already exists, because it reuses real content:

<h2 id="billing-heading">Billing address</h2>
<form aria-labelledby="billing-heading">

</form>

aria-describedby — points to an element that provides supplementary description, announced after the primary label. Used for hint text, error messages, or additional context:

<input type="email" id="email" aria-describedby="email-hint" />
<p id="email-hint">We will send your receipt to this address.</p>

aria-label wins over any visible text if both are present — which can create a mismatch between what sighted users read and what screen reader users hear. Prefer aria-labelledby to reference visible text, reserving aria-label for elements that genuinely have no visible label (icon buttons, landmark regions on pages with multiple of the same type).

States: keeping ARIA in sync

States reflect the current condition of an element and change as the user interacts with the page. The critical rule: ARIA states must always match the visual state. If the disclosure widget is open, aria-expanded must be "true". If it is closed, aria-expanded must be "false". A state that lags behind the visual creates a worse experience than no ARIA at all.

Common states and their usage:

<!-- A disclosure button — toggled by JavaScript -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>…</ul>
// When the button is clicked, update both the visual state and the ARIA state
button.addEventListener('click', () => {
  const isOpen = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!isOpen));
  menu.hidden = isOpen;
});

Other states you will encounter:

AttributeValuesUse case
aria-checked"true", "false", "mixed"Custom checkboxes and radio buttons
aria-disabled"true", "false"Elements that look disabled but remain focusable
aria-hidden"true"Remove decorative content from the accessibility tree
aria-invalid"true", "grammar", "spelling"Form validation errors

aria-hidden: a sharp tool

aria-hidden="true" removes an element and all its descendants from the accessibility tree — screen readers will not announce them. This is useful for decorative icons:

<!-- The SVG is decorative; the visible text label is sufficient -->
<button>
  <svg aria-hidden="true" focusable="false">…</svg>
  Save
</button>

The trap: never apply aria-hidden="true" to a focusable element. If a user can Tab to it, the screen reader will announce it regardless of aria-hidden — and the resulting announcement will be empty or confusing. If you want to hide something from the tab order too, also set tabindex="-1" and remove any interactive children from focus.

Live regions

A is an element the browser monitors for changes and automatically announces to screen readers. This is how dynamic content — search results loading, error messages appearing, a shopping cart updating — gets communicated without the user navigating to it:

<!-- polite: announced after the current task finishes -->
<div aria-live="polite" aria-atomic="true">
  <p id="status"></p>
</div>

<!-- assertive: interrupts immediately — reserve for critical errors only -->
<div role="alert"><!-- content injected by JS here --></div>
// Injecting content into a live region triggers the announcement
document.getElementById('status').textContent = '3 results found.';

Use aria-live="polite" for non-urgent updates. Use role="alert" (which implies aria-live="assertive") only for errors or critical messages — it interrupts whatever the user is doing.

Where to go next

You can now communicate role, state, and properties to assistive technologies. Next: Color and Contrast — the mathematical standard for ensuring text remains readable for users with low vision.

Finished reading? Mark it complete to track your progress.

On this page