Skip to main content

Welcome to our new website! The content and organization have been heavily redone and we want to hear from you! 
Submit feedback

ARIA Patterns & Components

Build accessible interactive components that work for everyone.

🧩 For developers

The first rule of ARIA

"No ARIA is better than bad ARIA."

Before reaching for ARIA, ask:

  1. Can I use a native HTML element? Native elements have built-in accessibility.
  2. Can I style a native element? A styled <button> beats a <div role="button">.
  3. Do I really need this custom widget? Simpler is often better.

Use ARIA when HTML alone can't express the semantics you needβ€”but use it correctly.

Essential ARIA attributes

Labeling

AttributePurposeExample
aria-labelProvides accessible name when no visible label<button aria-label="Close">Γ—</button>
aria-labelledbyPoints to element(s) that label this element<div aria-labelledby="heading1">
aria-describedbyPoints to element with additional description<input aria-describedby="hint1">

State & properties

AttributePurposeValues
aria-expandedIndicates expandable element statetrue / false
aria-selectedIndicates selection statetrue / false
aria-checkedCheckbox/toggle statetrue / false / mixed
aria-pressedToggle button statetrue / false
aria-hiddenHides from assistive tech (not visual)true / false
aria-liveAnnounces dynamic content changespolite / assertive / off

Pattern: Button

First choice: Use <button> element.

If you must use a div/span:

<div role="button" 
     tabindex="0"
     onclick="handleClick()"
     onkeydown="handleKeyDown(event)">
  Click me
</div>

<script>
function handleKeyDown(event) {
  // Buttons respond to Enter and Space
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    handleClick();
  }
}
</script>

Requirements checklist

  • ☐ role="button"
  • ☐ tabindex="0" (keyboard focusable)
  • ☐ Handle Enter and Space key
  • ☐ Visible focus indicator
  • ☐ Accessible name (text content or aria-label)

Pattern: Modal dialog

HTML structure

<div role="dialog" 
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
  <h2 id="dialog-title">Confirm deletion</h2>
  <p id="dialog-desc">This action cannot be undone.</p>
  <button>Delete</button>
  <button>Cancel</button>
</div>

Keyboard behavior

  • Tab β€” Cycle through focusable elements inside dialog only
  • Escape β€” Close dialog
  • Focus trapped inside while open
  • Focus returns to trigger element when closed

JavaScript requirements

function openModal(modal, triggerButton) {
  // 1. Show modal
  modal.style.display = 'block';
  
  // 2. Store trigger for later
  modal.triggerElement = triggerButton;
  
  // 3. Move focus to first focusable element
  const firstFocusable = modal.querySelector('button, [href], input');
  firstFocusable.focus();
  
  // 4. Trap focus
  modal.addEventListener('keydown', trapFocus);
}

function closeModal(modal) {
  modal.style.display = 'none';
  modal.triggerElement.focus(); // Return focus
}

function trapFocus(event) {
  if (event.key !== 'Tab') return;
  
  const focusables = modal.querySelectorAll('button, [href], input');
  const first = focusables[0];
  const last = focusables[focusables.length - 1];
  
  if (event.shiftKey && document.activeElement === first) {
    event.preventDefault();
    last.focus();
  } else if (!event.shiftKey && document.activeElement === last) {
    event.preventDefault();
    first.focus();
  }
}

Pattern: Tabs

HTML structure

<div class="tabs">
  <div role="tablist" aria-label="Course sections">
    <button role="tab" 
            id="tab-1" 
            aria-selected="true" 
            aria-controls="panel-1">
      Overview
    </button>
    <button role="tab" 
            id="tab-2" 
            aria-selected="false" 
            aria-controls="panel-2"
            tabindex="-1">
      Schedule
    </button>
    <button role="tab" 
            id="tab-3" 
            aria-selected="false" 
            aria-controls="panel-3"
            tabindex="-1">
      Resources
    </button>
  </div>
  
  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
    Overview content...
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
    Schedule content...
  </div>
  <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
    Resources content...
  </div>
</div>

Keyboard behavior

KeyAction
TabMove into tablist, then to panel
← β†’Move between tabs
HomeFirst tab
EndLast tab

Key implementation detail

Use roving tabindex: Only the selected tab has tabindex="0"; others have tabindex="-1". Arrow keys move focus and update tabindex.

Pattern: Accordion

HTML structure

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="sect1">
      Section 1
    </button>
  </h3>
  <div id="sect1" role="region" aria-labelledby="sect1-btn">
    Section 1 content...
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="sect2">
      Section 2
    </button>
  </h3>
  <div id="sect2" role="region" aria-labelledby="sect2-btn" hidden>
    Section 2 content...
  </div>
</div>

Key points

  • Buttons inside headings (maintain heading structure)
  • aria-expanded reflects state
  • aria-controls links to panel
  • hidden attribute hides collapsed panels

Pattern: Dropdown menu

HTML structure

<div class="dropdown">
  <button aria-haspopup="true" 
          aria-expanded="false"
          aria-controls="menu1">
    Actions β–Ό
  </button>
  <ul role="menu" id="menu1" hidden>
    <li role="menuitem"><a href="#">Edit</a></li>
    <li role="menuitem"><a href="#">Duplicate</a></li>
    <li role="separator"></li>
    <li role="menuitem"><a href="#">Delete</a></li>
  </ul>
</div>

Keyboard behavior

KeyAction
Enter / SpaceOpen menu, focus first item
↓Open menu or next item
↑Previous item
EscapeClose menu, focus trigger
HomeFirst item
EndLast item

Pattern: Live regions

Announce dynamic content changes to screen reader users.

Types

<!-- Polite: Waits for pause in speech -->
<div aria-live="polite">
  3 items in cart
</div>

<!-- Assertive: Interrupts immediately -->
<div role="alert">
  Error: Please fill in all required fields
</div>

<!-- Status: For status updates -->
<div role="status">
  Saving...
</div>

Best practices

  • Live region must exist in DOM before content changes
  • Use polite for most updates
  • Reserve assertive / alert for errors
  • Keep messages concise
  • Don't overuse β€” too many announcements are overwhelming

Testing your ARIA

Browser DevTools

  • Chrome: Elements β†’ Accessibility tab shows computed name/role
  • Firefox: Accessibility Inspector shows full a11y tree

Screen reader testing

Test these scenarios:

  • Can user identify what the element is?
  • Can user determine current state?
  • Can user operate the widget?
  • Are changes announced appropriately?

Common ARIA mistakes

MistakeFix
Using role on wrong elementCheck allowed child roles
Duplicate IDs for aria-labelledbyEnsure IDs are unique
aria-hidden on focusable elementAlso set tabindex="-1"
Missing required ARIA attributesCheck role requirements

Resources