Saved
Frontend Pattern

ARIA Roles and Attributes

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

Difficulty Beginner

By Den Odell

ARIA Roles and Attributes

Problem

When semantic HTML alone cannot convey the full meaning or state of UI components, assistive technologies like screen readers lack the information needed to help users navigate and interact with the interface. A <div> styled as a button provides no indication to screen readers that it’s interactive, while dynamic content that updates without page refresh remains invisible to assistive technologies that don’t automatically detect DOM changes. Custom widgets like tab panels, tree views, and comboboxes have no native HTML equivalents, leaving screen reader users unable to understand their structure or operate them effectively. Without proper semantic information, these complex interactions become inaccessible to users who rely on assistive technologies to understand interface state, component relationships, and available actions.

Solution

Add ARIA (Accessible Rich Internet Applications) roles to define widget types (dialog, menu, tab, tabpanel) and ARIA attributes to communicate state (aria-expanded, aria-selected, aria-checked) and properties (aria-label, aria-describedby, aria-labelledby, aria-controls).

ARIA roles override the default semantic meaning of elements, while ARIA attributes provide additional context about state, properties, and relationships. Use ARIA state attributes like aria-pressed and aria-current to communicate dynamic changes that occur through JavaScript interactions.

Apply landmark roles (banner, navigation, main, complementary) to define page structure when semantic HTML elements like <nav> and <main> cannot be used. This gives screen readers the semantic information they need to describe and operate custom components accurately.

Example

This example demonstrates using ARIA attributes to create an accessible dropdown menu that communicates its state and purpose to screen readers.

<!-- Button that controls a dropdown menu -->
<button
  aria-expanded="false"         <!-- Indicates the menu is currently collapsed -->
  aria-controls="dropdown-menu" <!-- Links this button to the menu it controls -->
  aria-haspopup="menu"          <!-- Indicates this button opens a menu -->
  aria-label="User settings"    <!-- Provides descriptive label for screen readers -->
>
  Settings
</button>

<!-- The dropdown menu itself -->
<ul id="dropdown-menu" role="menu" aria-hidden="true">
  <li role="menuitem">Profile</li> <!-- Each item is identified as a menu item -->
  <li role="menuitem">Settings</li>
  <li role="separator"></li>        <!-- Visual divider also marked semantically -->
  <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>
  <button
    role="tab"
    aria-selected="false"
    aria-controls="billing-panel"
    id="billing-tab"
    tabindex="-1"
  >
    Billing
  </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>

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

When ARIA is Unnecessary

Native semantic HTML elements like <button>, <nav>, <main>, <dialog>, <details>, and <select> already provide proper semantics to assistive technologies. Adding ARIA roles to these elements is redundant and should be avoided.

Live Region for Dynamic Updates

<!-- Status messages that should be announced to screen readers -->
<div
  role="status"
  aria-live="polite"
  aria-atomic="true"
  id="status-message"
>
  <!-- Updates here will be announced without stealing focus -->
</div>

<script>
function updateStatus(message) {
  document.getElementById('status-message').textContent = message;
  // Screen reader will announce: "5 items added to cart"
}
</script>

Custom Combobox (No Native HTML Equivalent)

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

Dynamic State Updates with JavaScript

function toggleDropdown(buttonElement) {
  const menuId = buttonElement.getAttribute('aria-controls');
  const menuElement = document.getElementById(menuId);
  const isExpanded = buttonElement.getAttribute('aria-expanded') === 'true';

  // Update ARIA attributes to reflect new state
  buttonElement.setAttribute('aria-expanded', !isExpanded);
  menuElement.setAttribute('aria-hidden', isExpanded);

  // Toggle visibility
  menuElement.style.display = isExpanded ? 'none' : 'block';
  
  // Screen readers will announce the state change
}

function selectTab(tabElement) {
  const tablist = tabElement.closest('[role="tablist"]');
  const allTabs = tablist.querySelectorAll('[role="tab"]');
  const allPanels = document.querySelectorAll('[role="tabpanel"]');

  // Deselect all tabs
  allTabs.forEach(tab => {
    tab.setAttribute('aria-selected', 'false');
    tab.setAttribute('tabindex', '-1');
  });

  // Hide all panels
  allPanels.forEach(panel => {
    panel.hidden = true;
  });

  // Select clicked tab
  tabElement.setAttribute('aria-selected', 'true');
  tabElement.setAttribute('tabindex', '0');

  // Show corresponding panel
  const panelId = tabElement.getAttribute('aria-controls');
  const panel = document.getElementById(panelId);
  panel.hidden = false;
  panel.focus();
}

Landmark Roles for Page Structure

<!-- When semantic HTML cannot be used -->
<div role="banner">
  <!-- Site header content -->
</div>

<div role="navigation" aria-label="Main navigation">
  <!-- Navigation links -->
</div>

<div role="main">
  <!-- Primary page content -->
</div>

<div role="complementary" aria-labelledby="sidebar-heading">
  <h2 id="sidebar-heading">Related Articles</h2>
  <!-- Sidebar content -->
</div>

<div role="contentinfo">
  <!-- Site footer -->
</div>

Custom Toggle Button

<button
  role="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'
  );
  // Screen reader announces "Dark mode toggle, pressed" or "not pressed"
}
</script>

Benefits

  • Makes custom widgets understandable to screen readers and assistive technologies by providing semantic context that HTML alone cannot express.
  • Communicates dynamic state changes that occur through JavaScript interactions without requiring page refreshes or user navigation.
  • Essential for creating accessible complex components like tabs, accordions, tree views, and comboboxes that have no native HTML equivalents.
  • Provides semantic context for elements that have no native HTML equivalent, enabling screen readers to describe and operate custom interactive components.
  • Improves keyboard navigation by clarifying component relationships through attributes like aria-controls and aria-owns.
  • Enables screen reader users to navigate page structure efficiently using landmark roles that identify major sections like navigation, main content, and complementary information.
  • Allows developers to build modern, interactive interfaces without sacrificing accessibility for users who depend on assistive technologies.

Tradeoffs

  • Incorrect ARIA is worse than no ARIA - misused roles and attributes create confusing experiences that mislead screen reader users about component purpose and behavior, potentially making interfaces less accessible than if ARIA were omitted entirely.
  • Requires deep understanding of accessibility specifications to use properly - the ARIA specification is complex with subtle rules about which roles can contain other roles, which attributes are valid for each role, and how different combinations interact.
  • Easy to misuse or apply redundantly when semantic HTML would suffice - adding role="button" to a <button> element or role="navigation" to a <nav> element is unnecessary and can create maintenance overhead without improving accessibility.
  • Testing requires actual screen reader usage to verify correct behavior - automated tools can detect missing or invalid ARIA attributes but cannot verify that the experience makes sense to screen reader users or that state changes announce properly.
  • Browser and screen reader support varies for less common ARIA attributes - while major roles and attributes have good support, newer or less common attributes may not work consistently across different screen reader and browser combinations.
  • ARIA does not provide keyboard behavior or visual styling - adding role="button" to a <div> makes it announce as a button but does not make it keyboard accessible or look like a button, requiring additional JavaScript and CSS implementation.
  • Overuse of ARIA can create verbose screen reader announcements - excessive attributes like multiple aria-describedby references or redundant labels can overwhelm users with too much information.
  • The aria-hidden="true" attribute removes content from the accessibility tree entirely, including all descendants - this can accidentally hide important content from screen readers if applied to parent elements containing focusable content.
  • Dynamic ARIA updates may not announce reliably without explicit live regions - changing aria-label or aria-expanded values through JavaScript does not guarantee screen readers will announce the change unless combined with aria-live or focus management techniques.
  • Some ARIA roles have strict parent-child requirements - using role="menuitem" without a parent role="menu" or role="tab" without a parent role="tablist" creates invalid structures that screen readers may handle inconsistently.
Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.