Every ARIA Attribute in HTML

Introduction

ARIA — Accessible Rich Internet Applications — is a specification that extends HTML with a set of attributes that communicate information to assistive technologies when native HTML semantics are insufficient. Screen readers, switch access devices, voice control software, and other assistive technologies rely on this information to understand what an element is, what state it is in, and how the user can interact with it.

The most important thing to know about ARIA before reading this reference is the first rule of ARIA use:

Do not use ARIA if you can use a native HTML element or attribute instead.

A <button> element is already a button to a screen reader. Adding role="button" to a <div> creates something that looks like a button to assistive technology but lacks keyboard operability, focus management, and activation behaviour — all of which you then have to implement manually. Native HTML is always preferable.

ARIA fills the gap for things native HTML cannot express — dynamic state changes, complex interactive widgets, live region announcements, and relationships between elements that are not adjacent in the DOM. Used correctly, ARIA makes the difference between an application that works for everyone and one that works only for sighted mouse users. Used incorrectly, it actively makes things worse — a screen reader user confronted with incorrect ARIA information is worse off than one confronted with no ARIA at all.

This reference covers every ARIA attribute in the specification, organised by category, with usage examples, common mistakes, and the patterns that actually work in production.

How ARIA Works — The Accessibility Tree

ARIA attributes modify the accessibility tree — a parallel representation of the DOM that assistive technologies consume. The accessibility tree contains four things for each node:

  • Role — what the element is (button, dialog, navigation, alert)
  • Name — what it is called (the label a screen reader announces)
  • State — its current condition (expanded, checked, disabled, invalid)
  • Properties — additional descriptive information (description, required, owns)

ARIA attributes modify these four things. Nothing else. ARIA does not add behaviour, it does not add keyboard interaction, it does not make elements focusable. It only adds information to the accessibility tree.

<!-- What the browser exposes to assistive technology for this element -->
<button
  aria-expanded="true"
  aria-controls="menu-dropdown"
  aria-label="Open navigation menu"
>
  <!-- Accessibility tree node:
  Role:  button (from native <button> element)
  Name:  "Open navigation menu" (from aria-label)
  State: expanded = true (from aria-expanded)
  Props: controls = "menu-dropdown" (from aria-controls)
--></button>

Category One: Widget Roles

Roles define what an element is. These are the most commonly needed roles when native HTML elements cannot serve the purpose.

role=“button”

Use only when a non-button element must behave as a button and using <button> is genuinely not possible.

<!-- ✅ Prefer this always -->
<button type="button" onclick="handleClick()">Save changes</button>

<!-- ❌ Avoid this — you must add tabindex, keyboard handlers, and focus styles manually -->
<div
  role="button"
  tabindex="0"
  onclick="handleClick()"
  onkeydown="handleKey(event)"
>
  Save changes
</div>

<!-- The only legitimate use: a framework component that renders a div
     and cannot be changed to render a button -->

role=“checkbox”

<!-- Custom checkbox with correct ARIA -->
<div
  role="checkbox"
  aria-checked="true"
  tabindex="0"
  aria-labelledby="terms-label"
  onkeydown="if(event.key===' '||event.key==='Enter') toggle(this)"
></div>
<span id="terms-label">I accept the terms and conditions</span>

<!-- Three states for indeterminate (e.g. select-all with partial selection) -->
<div role="checkbox" aria-checked="mixed" tabindex="0">Select all</div>

role=“radio” and role=“radiogroup”

<div role="radiogroup" aria-labelledby="priority-label">
  <p id="priority-label">Ticket priority</p>

  <div role="radio" aria-checked="false" tabindex="0">Low</div>
  <div role="radio" aria-checked="true" tabindex="-1">Medium</div>
  <div role="radio" aria-checked="false" tabindex="-1">High</div>
  <!-- Note: only one tabindex="0" in the group — use roving tabindex -->
</div>

role=“switch”

A binary on/off toggle — semantically different from a checkbox.

<button
  role="switch"
  aria-checked="false"
  onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
>
  Dark mode
</button>
<!-- Screen reader announces: "Dark mode, switch, off" -->
<!-- After toggle: "Dark mode, switch, on" -->

role=“slider”

<div
  role="slider"
  aria-valuenow="65"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuetext="65 percent"
  aria-label="Volume"
  tabindex="0"
></div>

role=“tab”, role=“tablist”, role=“tabpanel”

The complete tab pattern — one of the most commonly implemented custom widgets:

<div role="tablist" aria-label="Ticket management views">
  <button
    role="tab"
    aria-selected="true"
    aria-controls="panel-open"
    id="tab-open"
    tabindex="0"
  >
    Open
  </button>
  <button
    role="tab"
    aria-selected="false"
    aria-controls="panel-resolved"
    id="tab-resolved"
    tabindex="-1"
  >
    Resolved
  </button>
</div>

<div role="tabpanel" id="panel-open" aria-labelledby="tab-open" tabindex="0">
  <!-- Open tickets content -->
</div>

<div
  role="tabpanel"
  id="panel-resolved"
  aria-labelledby="tab-resolved"
  tabindex="0"
  hidden
>
  <!-- Resolved tickets content -->
</div>

role=“dialog” and role=“alertdialog”

<!-- Modal dialog — focus must move inside when opened -->
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-description"
>
  <h2 id="dialog-title">Resolve ticket</h2>
  <p id="dialog-description">
    This action will mark the ticket as resolved and notify the account owner.
  </p>
  <!-- dialog content -->
  <button>Confirm</button>
  <button>Cancel</button>
</div>

<!-- Alert dialog — for actions requiring immediate confirmation -->
<!-- Screen reader announces the full content immediately when opened -->
<div
  role="alertdialog"
  aria-modal="true"
  aria-labelledby="alert-title"
  aria-describedby="alert-body"
>
  <h2 id="alert-title">Delete terminal record</h2>
  <p id="alert-body">This cannot be undone. Are you sure?</p>
  <button>Delete permanently</button>
  <button>Cancel</button>
</div>

role=“menu”, role=“menubar”, role=“menuitem”

<ul role="menu" aria-label="Terminal actions">
  <li role="menuitem" tabindex="-1">View details</li>
  <li role="menuitem" tabindex="-1">Assign engineer</li>
  <li role="menuitem" tabindex="-1" aria-disabled="true">Reset terminal</li>
</ul>

<!-- menuitemcheckbox and menuitemradio for selectable menu items -->
<li role="menuitemcheckbox" aria-checked="true">Show offline only</li>
<li role="menuitemradio" aria-checked="false">Sort by region</li>
<li role="menuitemradio" aria-checked="true">Sort by status</li>

role=“listbox” and role=“option”

For custom select-like components:

<div
  role="listbox"
  aria-labelledby="status-filter-label"
  aria-activedescendant="option-open"
>
  <p id="status-filter-label">Filter by status</p>
  <div role="option" id="option-all" aria-selected="false">All</div>
  <div role="option" id="option-open" aria-selected="true">Open</div>
  <div role="option" id="option-resolved" aria-selected="false">Resolved</div>
</div>

role=“combobox”

For search inputs with a dropdown suggestion list:

<div
  role="combobox"
  aria-expanded="true"
  aria-haspopup="listbox"
  aria-owns="suggestions-list"
>
  <input
    type="text"
    aria-autocomplete="list"
    aria-controls="suggestions-list"
    aria-activedescendant="suggestion-1"
  />
</div>

<ul role="listbox" id="suggestions-list">
  <li role="option" id="suggestion-1">FedEx Account #1042</li>
  <li role="option" id="suggestion-2">FedEx Account #1043</li>
</ul>

role=“grid”, role=“row”, role=“gridcell”, role=“columnheader”, role=“rowheader”

For interactive data grids where cells are editable or selectable — distinct from a data table:

<div role="grid" aria-label="Terminal health grid" aria-rowcount="500">
  <div role="row">
    <div role="columnheader" aria-sort="ascending">Terminal ID</div>
    <div role="columnheader" aria-sort="none">Status</div>
    <div role="columnheader" aria-sort="none">Region</div>
  </div>
  <div role="row" aria-rowindex="1">
    <div role="rowheader">ATM-001</div>
    <div role="gridcell">Online</div>
    <div role="gridcell">North</div>
  </div>
</div>

role=“tree”, role=“treeitem”

For hierarchical navigation structures:

<ul role="tree" aria-label="File structure">
  <li role="treeitem" aria-expanded="true" aria-level="1">
    src
    <ul role="group">
      <li role="treeitem" aria-level="2" aria-selected="false">
        <span>app.component.ts</span>
      </li>
      <li role="treeitem" aria-expanded="false" aria-level="2">
        features
        <ul role="group">
          <li role="treeitem" aria-level="3" aria-selected="true">tickets</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

role=“tooltip”

<button aria-describedby="save-tooltip">Save</button>
<div role="tooltip" id="save-tooltip">
  Save changes and notify assigned engineer
</div>
<!-- tooltip content is announced as a description, not a label -->

role=“progressbar”

<!-- Determinate progress — known completion percentage -->
<div
  role="progressbar"
  aria-valuenow="65"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="File upload progress"
  aria-valuetext="65% complete"
></div>

<!-- Indeterminate progress — unknown completion time -->
<div
  role="progressbar"
  aria-label="Loading terminal data"
  aria-valuetext="Loading..."
>
  <!-- omit aria-valuenow when progress is indeterminate -->
</div>

role=“spinbutton”

For numeric inputs that increment/decrement:

<div
  role="spinbutton"
  aria-valuenow="5"
  aria-valuemin="1"
  aria-valuemax="100"
  aria-label="Tickets per page"
  tabindex="0"
>
  5
</div>

role=“scrollbar”

<div
  role="scrollbar"
  aria-controls="content-area"
  aria-valuenow="30"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-orientation="vertical"
  tabindex="0"
></div>

role=“separator”

<!-- As a structural separator (no tabindex — purely presentational) -->
<div role="separator" aria-orientation="horizontal"></div>

<!-- As a focusable separator in a split panel (has tabindex — interactive) -->
<div
  role="separator"
  aria-orientation="vertical"
  tabindex="0"
  aria-valuenow="30"
  aria-valuemin="10"
  aria-valuemax="90"
  aria-label="Resize panels"
></div>

Category Two: Landmark and Structure Roles

These define the regions of a page that users can jump between with screen reader shortcuts.

<!-- The full landmark structure of a well-organised page -->
<header role="banner">
  <!-- page header — one per page -->
  <nav role="navigation">
    <!-- navigation — use aria-label if multiple navs -->
    <main role="main">
      <!-- main content — one per page -->
      <aside role="complementary">
        <!-- sidebar or supplementary content -->
        <footer role="contentinfo">
          <!-- page footer — one per page -->
          <section role="region" aria-labelledby="section-heading">
            <!-- named region -->
            <form role="form" aria-labelledby="form-heading">
              <!-- landmark form -->
              <div role="search" aria-label="Search terminals">
                <!-- search region -->
              </div>
            </form>
          </section>
        </footer>
      </aside>
    </main>
  </nav>
</header>

Note: these landmark roles are already implied by the corresponding HTML elements (<header>, <nav>, <main>, etc.). Only add the role attribute explicitly when you cannot use the semantic HTML element.

Category Three: Live Region Attributes

Live regions announce dynamic content changes to screen readers without requiring the user to move focus. They are essential for any interface that updates asynchronously.

aria-live

The most important live region attribute. Controls when announcements are made.

<!-- aria-live="polite" — announces when the user is idle -->
<!-- Use for: status messages, search results, non-urgent updates -->
<div aria-live="polite" aria-atomic="true" id="status-message">
  <!-- Updated dynamically: "Ticket resolved successfully" -->
</div>

<!-- aria-live="assertive" — interrupts immediately -->
<!-- Use for: errors, critical alerts, urgent system messages -->
<!-- Use sparingly — assertive interruptions are disruptive -->
<div aria-live="assertive" aria-atomic="true" id="error-message">
  <!-- Updated dynamically: "Session expired. Please log in again." -->
</div>

<!-- aria-live="off" — no automatic announcements (default) -->
<div aria-live="off">
  <!-- Changes are not announced -->
</div>
// Angular — announcing with LiveAnnouncer from @angular/cdk/a11y
import { LiveAnnouncer } from '@angular/cdk/a11y';

@Component({
  /* ... */
})
export class TicketComponent {
  constructor(private announcer: LiveAnnouncer) {}

  async resolveTicket(id: string) {
    await this.ticketService.resolve(id);
    // Polite announcement — waits for user to be idle
    this.announcer.announce('Ticket resolved successfully', 'polite');
  }

  async deleteTicket(id: string) {
    await this.ticketService.delete(id);
    // Assertive — interrupts immediately for critical feedback
    this.announcer.announce('Ticket deleted', 'assertive');
  }
}

aria-atomic

Controls whether the entire live region is announced or only the changed portion.

<!-- aria-atomic="true" — announces the entire region content when anything changes -->
<!-- Use when partial announcements would be confusing -->
<div aria-live="polite" aria-atomic="true">
  <span>3 terminals offline</span>
  <!-- announces the full sentence, not just "3" -->
</div>

<!-- aria-atomic="false" — announces only the changed nodes (default) -->
<div aria-live="polite" aria-atomic="false">
  <span>Status: </span>
  <span>Online</span>
  <!-- only "Online" is announced when it changes -->
</div>

aria-relevant

Controls which types of changes trigger an announcement.

<!-- aria-relevant="additions" — only announce when nodes are added (default for most cases) -->
<div aria-live="polite" aria-relevant="additions">
  <!-- New alert items appended here are announced -->
</div>

<!-- aria-relevant="removals" — announce when nodes are removed -->
<!-- Rare — screen readers do not usually need to hear what was removed -->

<!-- aria-relevant="text" — announce text content changes -->
<div aria-live="polite" aria-relevant="text additions">
  <!-- Both text changes and new nodes are announced -->
</div>

<!-- aria-relevant="all" — announce any change -->
<!-- Use very sparingly — extremely verbose -->

aria-busy

Signals that a region is loading and announcements should be held until ready.

<!-- Set to true while loading, false when complete -->
<div aria-live="polite" aria-busy="true" id="results-region">
  <!-- Loading... -->
</div>

<!-- When data arrives, set aria-busy="false" — then update content -->
<!-- Screen reader announces the complete content once, not incremental updates -->
<div aria-live="polite" aria-busy="false" id="results-region">
  47 terminals found across 3 regions
</div>

Category Four: Relationship Attributes

These create explicit relationships between elements that may not be adjacent in the DOM.

aria-labelledby

Points to the element(s) that label this element. More powerful than aria-label — the label can be visible text anywhere in the document, and multiple IDs can be combined.

<!-- Basic labelledby -->
<h2 id="section-title">Open Tickets</h2>
<div role="region" aria-labelledby="section-title">
  <!-- screen reader announces: "Open Tickets, region" -->
</div>

<!-- Combining multiple labels -->
<h2 id="account-name">Acme Corp</h2>
<h3 id="ticket-count">12 open tickets</h3>
<section aria-labelledby="account-name ticket-count">
  <!-- Announced: "Acme Corp 12 open tickets, region" -->
</section>

<!-- Dialog labeled by its heading -->
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
  <h2 id="modal-title">Assign ticket to engineer</h2>
  <!-- content -->
</div>

aria-label

Provides an accessible name directly on the element — use when no visible label text exists.

<!-- Icon-only button — no visible text, must have aria-label -->
<button aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false">
    <path d="M6 6 L18 18 M18 6 L6 18" />
    <!-- X icon -->
  </svg>
</button>

<!-- Multiple regions of the same type — distinguish with aria-label -->
<nav aria-label="Primary navigation">...</nav>
<nav aria-label="Breadcrumb navigation">...</nav>
<nav aria-label="Pagination">...</nav>

<!-- Search landmark -->
<div role="search" aria-label="Search terminals">
  <input type="search" placeholder="Search by ID or location" />
</div>

<!-- ❌ Do not use aria-label and aria-labelledby together on the same element -->
<!-- aria-labelledby takes precedence and aria-label is ignored -->

aria-describedby

Points to element(s) that provide additional descriptive context — announced after the label and role.

<!-- Form field with hint and error -->
<label for="ticket-subject">Subject</label>
<input
  type="text"
  id="ticket-subject"
  aria-describedby="subject-hint subject-error"
  aria-invalid="true"
/>
<span id="subject-hint">Maximum 200 characters</span>
<span id="subject-error" role="alert">Subject is required</span>
<!-- Announced: "Subject, edit text, invalid. Maximum 200 characters. Subject is required." -->

<!-- Button with additional context -->
<button aria-describedby="delete-warning">Delete terminal record</button>
<p id="delete-warning" class="sr-only">
  This permanently removes the terminal from all monitoring dashboards.
</p>

aria-controls

Identifies the element(s) that this element controls.

<!-- Accordion — button controls the panel -->
<button aria-expanded="false" aria-controls="faq-answer-1">
  What is a terminal health alert?
</button>
<div id="faq-answer-1" hidden>A terminal health alert is triggered when...</div>

<!-- Tab controls its panel -->
<button
  role="tab"
  aria-selected="true"
  aria-controls="panel-overview"
  id="tab-overview"
>
  Overview
</button>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
  <!-- panel content -->
</div>

aria-owns

Declares ownership of elements that are not DOM children — useful when CSS positioning breaks the parent-child relationship.

<!-- Dropdown menu rendered in a portal at the body level
     but logically owned by the button that opens it -->
<button aria-haspopup="true" aria-expanded="true" aria-owns="floating-menu">
  Actions
</button>

<!-- Rendered at body level by a portal/overlay system -->
<ul id="floating-menu" role="menu">
  <li role="menuitem">Edit</li>
  <li role="menuitem">Delete</li>
</ul>

aria-activedescendant

Used in composite widgets (listbox, combobox, grid) to indicate which descendant is currently “active” without moving DOM focus. Focus stays on the container; aria-activedescendant points to the active item.

<input
  type="text"
  role="combobox"
  aria-expanded="true"
  aria-autocomplete="list"
  aria-controls="search-suggestions"
  aria-activedescendant="suggestion-2"
/>
<!-- focus remains on input -->

<ul id="search-suggestions" role="listbox">
  <li role="option" id="suggestion-1">ATM-001</li>
  <li role="option" id="suggestion-2" aria-selected="true">ATM-002</li>
  <!-- aria-activedescendant points here — announced without moving focus -->
  <li role="option" id="suggestion-3">ATM-003</li>
</ul>

aria-details

Points to an element with extended description — more detailed than aria-describedby. Used for complex content like figures, charts, or data tables where a full prose description is needed.

<img
  src="terminal-status-chart.png"
  alt="Terminal status distribution"
  aria-details="chart-details"
/>

<div id="chart-details">
  <p>This chart shows terminal status across all regions:</p>
  <ul>
    <li>North: 45 online, 3 offline</li>
    <li>South: 38 online, 7 offline</li>
    <li>East: 52 online, 1 offline</li>
  </ul>
</div>

aria-flowto

Overrides the default reading order — allows you to specify a non-DOM-order sequence for screen reader navigation. Use very sparingly.

<!-- Reading order: intro → step-1 → step-2 → step-3 regardless of DOM order -->
<div id="intro" aria-flowto="step-1">Introduction content</div>
<div id="step-3">Step 3 content</div>
<!-- DOM order would read this second -->
<div id="step-1" aria-flowto="step-2">Step 1 content</div>
<div id="step-2" aria-flowto="step-3">Step 2 content</div>

Category Five: State Attributes

State attributes communicate the current condition of an element. They change dynamically as the user interacts with the application.

aria-expanded

Indicates whether a controlled element is expanded or collapsed.

<!-- Accordion -->
<button aria-expanded="false" aria-controls="answer-panel">
  How do I assign a ticket?
</button>
<div id="answer-panel" hidden>...</div>

<!-- Navigation with submenu -->
<button aria-expanded="true" aria-haspopup="true">Account settings</button>

<!-- Combobox -->
<input aria-expanded="true" aria-haspopup="listbox" role="combobox" />

aria-selected

Indicates the selected state in a selection widget.

<!-- Tabs -->
<button role="tab" aria-selected="true">Open</button>
<button role="tab" aria-selected="false">Resolved</button>

<!-- Listbox options -->
<li role="option" aria-selected="true">High priority</li>
<li role="option" aria-selected="false">Medium priority</li>

<!-- Grid row selection -->
<tr role="row" aria-selected="true">
  ...
</tr>

aria-checked

The checked state for checkboxes, radio buttons, switches, and menu items.

<!-- Three states for checkboxes -->
<div role="checkbox" aria-checked="true">Selected</div>
<div role="checkbox" aria-checked="false">Unselected</div>
<div role="checkbox" aria-checked="mixed">Partially selected</div>

<!-- Switch -->
<button role="switch" aria-checked="false">Notifications</button>

<!-- Menu checkbox -->
<li role="menuitemcheckbox" aria-checked="true">Show alerts</li>

aria-pressed

For toggle buttons — different from aria-checked in that it applies to button elements, not form controls.

<!-- Toggle button — not a switch, not a checkbox, a button with a pressed state -->
<button
  aria-pressed="false"
  onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
  Mute notifications
</button>
<!-- Announced: "Mute notifications, toggle button, not pressed" -->
<!-- After click: "Mute notifications, toggle button, pressed" -->

aria-disabled

Communicates a disabled state to assistive technology. Note the difference from the HTML disabled attribute: aria-disabled="true" keeps the element in the tab order and focusable — the user can still reach it and hear why it is disabled. HTML disabled removes it from the tab order entirely.

<!-- HTML disabled — removed from tab order, screen reader may skip it -->
<button disabled>Submit (form incomplete)</button>

<!-- aria-disabled="true" — focusable, screen reader announces "dimmed" or "unavailable" -->
<!-- Use when you want users to know the button exists but cannot be activated yet -->
<button aria-disabled="true" onclick="return false">
  Submit (complete all required fields to continue)
</button>

aria-hidden

Removes an element from the accessibility tree entirely. Everything inside is invisible to assistive technology.

<!-- Hide decorative icons from screen readers -->
<button>
  <svg aria-hidden="true" focusable="false"><!-- icon --></svg>
  Save changes
</button>

<!-- Hide decorative images -->
<img src="decorative-divider.svg" alt="" aria-hidden="true" />
<!-- Note: alt="" is correct for decorative images, aria-hidden redundant but harmless -->

<!-- Hide duplicate content -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">4 out of 5 stars</span>

<!-- ❌ Never use aria-hidden="true" on a focusable element -->
<!-- A keyboard user can still reach it but the screen reader announces nothing -->
<button aria-hidden="true">This is broken</button>

<!-- ❌ Never hide the focused element -->
<!-- Focus trap with aria-hidden creates an invisible keyboard hole -->

aria-invalid

Communicates validation state to assistive technology.

<!-- Invalid field -->
<input
  type="email"
  aria-invalid="true"
  aria-describedby="email-error"
  value="not-an-email"
/>
<span id="email-error" role="alert">Please enter a valid email address</span>

<!-- Grammar or spelling error (for rich text editors) -->
<span aria-invalid="grammar">their going to the meeting</span>
<span aria-invalid="spelling">teh annual report</span>

<!-- Explicitly valid — useful after successful validation -->
<input type="email" aria-invalid="false" value="user@example.com" />

aria-required

Indicates a field must be completed. Supplements the HTML required attribute for custom form controls.

<!-- HTML required — browsers show native validation UI -->
<input type="text" required aria-required="true" />
<!-- Note: aria-required is redundant here but harmless -->

<!-- Custom form control — no native required, must use aria-required -->
<div role="combobox" aria-required="true" aria-labelledby="status-label">
  <!-- custom select-like widget -->
</div>

aria-current

Indicates the current item in a set — particularly useful for navigation.

<!-- Current page in navigation -->
<nav aria-label="Primary">
  <a href="/dashboard">Dashboard</a>
  <a href="/tickets" aria-current="page">Tickets</a>
  <!-- currently active page -->
  <a href="/monitoring">Monitoring</a>
</nav>

<!-- Current step in a wizard -->
<ol aria-label="Account setup steps">
  <li><a href="#" aria-current="step">Profile</a></li>
  <li><a href="#">Preferences</a></li>
  <li><a href="#">Confirmation</a></li>
</ol>

<!-- Current date in a calendar -->
<td role="gridcell" aria-current="date">21</td>

<!-- Valid values: page, step, location, date, time, true -->

aria-grabbed and aria-dropeffect (deprecated)

These were part of the original drag-and-drop ARIA specification but have been deprecated. Use the HTML5 Drag and Drop API with appropriate event handling and live region announcements instead.

Category Six: Property Attributes

Properties provide additional descriptive information that does not change frequently with interaction.

aria-haspopup

Indicates that an element opens a popup — a listbox, tree, grid, dialog, or menu.

<!-- Opens a menu -->
<button aria-haspopup="menu" aria-expanded="false">Actions</button>

<!-- Opens a listbox (select-like dropdown) -->
<button aria-haspopup="listbox" aria-expanded="false">Filter by status</button>

<!-- Opens a dialog -->
<button aria-haspopup="dialog">Create ticket</button>

<!-- Opens a tree -->
<button aria-haspopup="tree" aria-expanded="false">Browse categories</button>

<!-- Opens a grid -->
<button aria-haspopup="grid" aria-expanded="false">Date picker</button>

<!-- aria-haspopup="true" is equivalent to "menu" — be specific when possible -->

aria-level

Defines the heading level in a tree or outline structure — use when the visual heading level needs to differ from the semantic level.

<!-- Custom heading hierarchy in a widget -->
<div role="heading" aria-level="3">Terminal Region: North</div>
<div role="heading" aria-level="4">Fault Summary</div>

<!-- In a tree -->
<li role="treeitem" aria-level="1" aria-expanded="true">Regions</li>
<li role="treeitem" aria-level="2">North</li>
<li role="treeitem" aria-level="3">ATM Terminals</li>

aria-orientation

Communicates whether a widget is oriented horizontally or vertically — affects keyboard navigation expectations.

<!-- Vertical menu — Up/Down arrow keys navigate -->
<ul role="menu" aria-orientation="vertical">
  ...
</ul>

<!-- Horizontal toolbar — Left/Right arrow keys navigate -->
<div role="toolbar" aria-orientation="horizontal">...</div>

<!-- Vertical slider -->
<div
  role="slider"
  aria-orientation="vertical"
  aria-valuenow="50"
  aria-valuemin="0"
  aria-valuemax="100"
></div>

aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext

For range widgets — sliders, progress bars, spinbuttons, scrollbars.

<div
  role="slider"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuenow="65"
  aria-valuetext="65% — 325 of 500 tickets resolved"
  aria-label="Ticket resolution progress"
  tabindex="0"
></div>

<!-- aria-valuetext overrides aria-valuenow in the announcement
     Use it when the numeric value needs context to be meaningful -->
<!-- Without aria-valuetext: "65" -->
<!-- With aria-valuetext: "65% — 325 of 500 tickets resolved" -->

aria-multiselectable

Indicates that more than one item can be selected.

<div
  role="listbox"
  aria-multiselectable="true"
  aria-label="Select regions to monitor"
>
  <div role="option" aria-selected="true">North</div>
  <div role="option" aria-selected="true">South</div>
  <div role="option" aria-selected="false">East</div>
</div>

aria-multiline

Indicates whether a textbox accepts multiple lines.

<div
  role="textbox"
  aria-multiline="true"
  aria-label="Ticket description"
  contenteditable="true"
></div>
<!-- Tells screen reader: Enter key adds a new line, not submits -->

aria-placeholder

Provides hint text for empty inputs in custom form controls.

<!-- For native inputs, use the HTML placeholder attribute -->
<input type="text" placeholder="Search by terminal ID" />

<!-- For custom textbox roles, use aria-placeholder -->
<div
  role="textbox"
  aria-placeholder="Search by terminal ID"
  aria-labelledby="search-label"
  contenteditable="true"
></div>

aria-readonly

Indicates a value can be read but not modified.

<!-- HTML readonly — works for native inputs -->
<input type="text" readonly value="Account #1042" />

<!-- aria-readonly — for custom widgets -->
<div role="textbox" aria-readonly="true" aria-label="Account number">
  Account #1042
</div>
<div role="spinbutton" aria-readonly="true" aria-valuenow="5">5</div>

aria-sort

Indicates the sort direction of a column header.

<table>
  <tr>
    <th aria-sort="ascending">Terminal ID</th>
    <!-- sorted A→Z -->
    <th aria-sort="descending">Last Updated</th>
    <!-- sorted Z→A -->
    <th aria-sort="none">Status</th>
    <!-- sortable but not sorted -->
    <th>Region</th>
    <!-- not sortable — no aria-sort -->
  </tr>
</table>

aria-keyshortcuts

Documents keyboard shortcuts associated with an element.

<button aria-keyshortcuts="Control+S">Save</button>
<button aria-keyshortcuts="Alt+N">New ticket</button>
<!-- Announced to screen reader users so they know shortcuts exist -->

aria-roledescription

Provides a human-readable description of a role to replace the default role announcement.

<!-- Changes "slide" role announcement to something more meaningful -->
<div
  role="group"
  aria-roledescription="slide"
  aria-label="Terminal overview — slide 1 of 5"
>
  <!-- carousel slide content -->
</div>

<!-- ❌ Do not use to rename semantic roles in misleading ways -->
<!-- ❌ <button aria-roledescription="link"> — confusing and incorrect -->

aria-colcount, aria-rowcount, aria-colindex, aria-rowindex, aria-colspan, aria-rowspan

For grids and tables where the full dataset is larger than what is rendered (virtual scrolling).

<!-- Grid with 500 rows but only 20 rendered at a time -->
<div role="grid" aria-rowcount="500" aria-colcount="6">
  <div role="row" aria-rowindex="41">
    <!-- currently showing rows 41-60 -->
    <div role="gridcell" aria-colindex="1">ATM-041</div>
    <div role="gridcell" aria-colindex="2">Online</div>
  </div>
</div>

<!-- Merged cells in a grid -->
<div role="columnheader" aria-colspan="2" aria-colindex="3">Location</div>

aria-setsize and aria-posinset

For items in a set when the full set is not rendered (virtual scrolling, pagination).

<!-- Showing items 41-60 of 500 total -->
<li role="option" aria-setsize="500" aria-posinset="41">ATM-041</li>
<li role="option" aria-setsize="500" aria-posinset="42">ATM-042</li>
<!-- Screen reader announces: "ATM-041, 41 of 500" -->

aria-errormessage

Points to the element containing an error message — more specific than aria-describedby for validation errors.

<input type="email" aria-invalid="true" aria-errormessage="email-error-msg" />

<span id="email-error-msg" role="alert">
  Please enter a valid email address
</span>

<!-- Note: aria-errormessage is only announced when aria-invalid is true -->
<!-- The referenced element should not use display:none — use visibility:hidden
     or clip the element off-screen if you want it invisible but announced -->

aria-modal

Indicates that a dialog is modal — content outside should be treated as inert.

<div role="dialog" aria-modal="true" aria-labelledby="dialog-heading">
  <h2 id="dialog-heading">Confirm resolution</h2>
  <!-- With aria-modal="true", screen readers should not browse outside the dialog -->
  <!-- Note: some screen readers require JavaScript focus trapping as well -->
</div>

Category Seven: Visually Hidden Content

Not technically an ARIA attribute, but the pattern used everywhere alongside ARIA — content that is visually hidden but present in the accessibility tree.

/* The correct visually-hidden class */
/* clip-path: inset(50%) is more reliable than clip: rect(0,0,0,0) in modern browsers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Visible on focus — for skip links and focus-activated content */
.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  padding: inherit;
  margin: inherit;
  overflow: visible;
  clip-path: none;
  white-space: normal;
}
<!-- Common uses of visually hidden content -->

<!-- Skip link — visible on focus, hidden otherwise -->
<a href="#main-content" class="sr-only sr-only-focusable"
  >Skip to main content</a
>

<!-- Icon button label -->
<button>
  <svg aria-hidden="true"><!-- close icon --></svg>
  <span class="sr-only">Close dialog</span>
</button>

<!-- Additional context for screen readers -->
<a href="/tickets/1042">
  View details
  <span class="sr-only">for ticket #1042: Login broken</span>
</a>
<!-- Without sr-only: "View details" — ambiguous with multiple links -->
<!-- With sr-only: "View details for ticket #1042: Login broken" — clear -->

<!-- Star rating -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">Rated 4 out of 5 stars</span>

Quick Reference Table

AttributeCategoryPurpose
aria-labelNamingProvides accessible name directly
aria-labelledbyNamingPoints to element that provides the name
aria-describedbyDescriptionPoints to element with additional description
aria-detailsDescriptionPoints to element with extended description
aria-liveLive regionsAnnounces dynamic content changes
aria-atomicLive regionsAnnounce whole region or just changed part
aria-relevantLive regionsWhich change types trigger announcement
aria-busyLive regionsHold announcements while loading
aria-expandedStateWhether controlled content is expanded
aria-selectedStateSelected state in selection widgets
aria-checkedStateChecked state for checkboxes/switches
aria-pressedStatePressed state for toggle buttons
aria-disabledStateDisabled but remains in tab order
aria-hiddenStateRemoves element from accessibility tree
aria-invalidStateValidation failure state
aria-requiredStateField must be completed
aria-currentStateCurrent item in a set
aria-haspopupPropertiesElement opens a popup
aria-controlsRelationshipPoints to element this controls
aria-ownsRelationshipDeclares ownership of non-child elements
aria-activedescendantRelationshipActive item in composite widget
aria-flowtoRelationshipOverrides reading order
aria-sortPropertiesColumn sort direction
aria-orientationPropertiesWidget axis orientation
aria-multiselectablePropertiesMultiple selection allowed
aria-multilinePropertiesTextbox accepts multiple lines
aria-placeholderPropertiesHint text for empty custom inputs
aria-readonlyPropertiesValue readable but not editable
aria-requiredPropertiesInput must have a value
aria-levelPropertiesHeading level in tree/outline
aria-valueminRangeMinimum value
aria-valuemaxRangeMaximum value
aria-valuenowRangeCurrent value
aria-valuetextRangeHuman-readable value description
aria-setsizeSetTotal items in set
aria-posinsetSetPosition of item in set
aria-rowcountGridTotal rows in grid
aria-colcountGridTotal columns in grid
aria-rowindexGridRow position
aria-colindexGridColumn position
aria-rowspanGridRows spanned by cell
aria-colspanGridColumns spanned by cell
aria-modalDialogContent outside is inert
aria-errormessageValidationPoints to error message element
aria-keyshortcutsPropertiesDocuments keyboard shortcuts
aria-roledescriptionPropertiesCustom role announcement text

Conclusion

ARIA is a powerful tool and an easy one to misuse. The attributes in this reference exist because native HTML cannot express every interactive pattern that modern applications require — but native HTML should always be the first choice, and ARIA the fallback when it genuinely cannot do the job.

The most common mistake is using ARIA to describe what something looks like rather than what it is. A div with a coloured background is not a status indicator to a screen reader — it is nothing. An element with role="status" and content that describes the state is something a screen reader can work with.

The second most common mistake is applying ARIA attributes and then not testing them with an actual screen reader. Automated tools catch missing labels and incorrect nesting. They do not catch announcements that are technically correct but confusing in context, or live regions that fire at the wrong time, or focus management that works in the DOM but not in practice.

Use this reference to understand what is available. Use a screen reader to verify that what you built actually works.