Skip to main content
Saved
Pattern
Difficulty Beginner

ARIA Roles and Attributes

Use ARIA roles and attributes to provide semantic information to assistive technologies when HTML semantics are insufficient.

By Den Odell Added

ARIA Roles and Attributes

Problem

You’ve built a custom dropdown menu with divs and CSS. It looks beautiful and works with a mouse, but fire up a screen reader and it’s silent. No mention that it’s a menu, no indication items are selectable, no announcement when the selection changes.

I’ve tested interfaces where tab panels, tree views, and comboboxes were completely unusable with screen readers. Users couldn’t tell which tab was selected or understand the widget’s structure at all.

The core issue: native HTML can only express so much. There’s no <combobox> or <tabpanel> element, so when you build custom widgets, you need a way to tell assistive technologies what they are and how they behave.

Solution

ARIA tells screen readers what your custom widgets are and what they’re doing. Add roles like role="menu" or role="tab" to define widget types, and attributes like aria-expanded, aria-selected, and aria-checked to communicate state. When the user opens your dropdown, update aria-expanded="true"; when they select a tab, set aria-selected="true" on the active one and "false" on the rest. Screen readers announce these changes.

Use aria-label when visible text isn’t enough, aria-describedby to connect elements to descriptions, and aria-controls to indicate which element a button controls.

Important: ARIA is a last resort. Native HTML elements like <button>, <dialog>, and <details> already have the right semantics. Only reach for ARIA when HTML can’t express what you need.

Example

Here’s ARIA in action: a dropdown menu, tab interface, and live region that each tell screen readers exactly what’s happening.

<button
  aria-expanded="false"
  aria-controls="dropdown-menu"
  aria-haspopup="menu"
  aria-label="User settings"
>
  Settings
</button>

<ul id="dropdown-menu" role="menu" aria-hidden="true">
  <li role="menuitem">Profile</li>
  <li role="menuitem">Settings</li>
  <li role="separator"></li>
  <li role="menuitem">Logout</li>
</ul>

Tab Interface with ARIA

<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="profile-panel" id="profile-tab" tabindex="0">
    Profile
  </button>
  <button role="tab" aria-selected="false" aria-controls="security-panel" id="security-tab" tabindex="-1">
    Security
  </button>
</div>

<div role="tabpanel" id="profile-panel" aria-labelledby="profile-tab" tabindex="0">
  <!-- Profile content -->
</div>

<div role="tabpanel" id="security-panel" aria-labelledby="security-tab" tabindex="0" hidden>
  <!-- Security content -->
</div>

When ARIA is Unnecessary

Native HTML elements like <button>, <nav>, <dialog>, and <select> already provide proper semantics. Adding ARIA roles to them is redundant.

Live Region for Dynamic Updates

<div role="status" aria-live="polite" aria-atomic="true" id="status-message">
  <!-- Updates announced without stealing focus -->
</div>

<script>
function updateStatus(message) {
  document.getElementById('status-message').textContent = message;
}
</script>

Custom Combobox

<label id="combo-label">Choose a country</label>
<div role="combobox" aria-expanded="false" aria-haspopup="listbox" aria-labelledby="combo-label">
  <input type="text" aria-autocomplete="list" aria-controls="country-list" aria-activedescendant="" />
</div>
<ul role="listbox" id="country-list" aria-labelledby="combo-label">
  <li role="option" id="opt-1">United States</li>
  <li role="option" id="opt-2">Canada</li>
</ul>

Dynamic State Updates

function toggleDropdown(button) {
  const menu = document.getElementById(button.getAttribute('aria-controls'));
  const isExpanded = button.getAttribute('aria-expanded') === 'true';

  button.setAttribute('aria-expanded', !isExpanded);
  menu.setAttribute('aria-hidden', isExpanded);
  menu.style.display = isExpanded ? 'none' : 'block';
}

function selectTab(tab) {
  const tablist = tab.closest('[role="tablist"]');

  tablist.querySelectorAll('[role="tab"]').forEach(t => {
    t.setAttribute('aria-selected', 'false');
    t.setAttribute('tabindex', '-1');
  });

  tab.setAttribute('aria-selected', 'true');
  tab.setAttribute('tabindex', '0');

  const panel = document.getElementById(tab.getAttribute('aria-controls'));
  document.querySelectorAll('[role="tabpanel"]').forEach(p => p.hidden = true);
  panel.hidden = false;
}

Landmark Roles

<div role="banner"><!-- header --></div>
<div role="navigation" aria-label="Main"><!-- nav links --></div>
<div role="main"><!-- primary content --></div>
<div role="complementary"><!-- sidebar --></div>
<div role="contentinfo"><!-- footer --></div>

Toggle Button

<button aria-pressed="false" aria-label="Enable dark mode" onclick="toggleDarkMode(this)">
  <span aria-hidden="true">🌙</span>
</button>

<script>
function toggleDarkMode(button) {
  const isPressed = button.getAttribute('aria-pressed') === 'true';
  button.setAttribute('aria-pressed', !isPressed);
  button.setAttribute('aria-label', isPressed ? 'Enable dark mode' : 'Disable dark mode');
}
</script>

Benefits

  • Custom widgets become usable by screen readers. Your tab panel announces as a tab panel, not as random divs.
  • State changes get announced: when the dropdown opens, screen readers say so; when the selection changes, users know.
  • Complex components like comboboxes, tree views, and accordions become buildable with proper accessibility.
  • Relationships between elements become clear through attributes like aria-controls and aria-describedby, letting you build modern interfaces without leaving screen reader users behind.

Tradeoffs

  • Wrong ARIA is worse than no ARIA. Misuse roles and you’ve actively made things worse for screen reader users.
  • The specification is complex, with strict parent-child requirements (e.g., role="menuitem" needs a parent role="menu"). Role-specific attribute rules take time to learn.
  • Easy to overdo: adding role="button" to a <button> is redundant, and ARIA when HTML would suffice creates maintenance burden for no benefit.
  • ARIA doesn’t add behavior. role="button" on a div makes it announce as a button but doesn’t make it keyboard accessible. You still need JavaScript.
  • Automated testing only catches syntax errors. You need real screen reader testing to verify the experience makes sense, and support varies across NVDA, JAWS, and VoiceOver.
  • aria-hidden="true" removes content from screen readers entirely, including descendants. It’s easy to accidentally hide important content.
  • Dynamic updates may not announce; changing aria-expanded via JavaScript doesn’t guarantee an announcement unless you use live regions or move focus.

Summary

ARIA bridges the gap between custom UI and assistive technologies by providing semantic information HTML can’t convey. Use it to describe widget types, states, and relationships when building tabs, menus, or dialogs, but remember that native HTML elements already have these semantics built in. Reach for ARIA only when semantic HTML falls short.

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.