Accessibility Checker logoAccessibility Checker

ARIA patterns

ARIA Examples

Understand when ARIA helps, when it hurts, and how to write common ARIA patterns safely.

ARIASemanticsModalsAlertsNavigationTabsMenus

The First Rule of ARIA

Use native HTML before ARIA. ARIA can improve custom widgets, but it cannot repair broken keyboard behavior, missing focus management, or incorrect visual behavior.

  • Use button for actions, not div role="button".
  • Use a label element before aria-label when visible text is available.
  • Use aria-expanded only when a control actually expands or collapses content.
  • Use aria-current for the current page or current step, not for hover or focus.
  • Do not add roles or ARIA attributes that conflict with native HTML semantics.
  • Do not hide focusable content with aria-hidden="true".

Core ARIA Patterns to Know

A strong ARIA foundation includes both semantics and keyboard behavior. If the keyboard interaction is wrong, the ARIA pattern is not complete.

  • Disclosure and accordion: a real button toggles aria-expanded and controls a panel.
  • Dialog and modal: role="dialog" or native dialog needs a name, focus movement, Escape support, and focus return.
  • Tabs: tab, tablist, and tabpanel require active state, roving focus, and arrow-key navigation.
  • Menu button: use only for application-style command menus, not ordinary website navigation.
  • Combobox and listbox: expose the input, popup, active option, selected value, and keyboard model correctly.
  • Tooltip: provide supplemental information on focus and hover, never required instructions.
  • Live regions: announce important changes without flooding the user.
  • Form errors: aria-invalid and aria-describedby should point to useful text.

Good and Bad ARIA Examples

These examples cover the common places where ARIA helps and the places where it can create worse accessibility.

Disclosure button

Pass

A button toggles aria-expanded and controls a visible panel with aria-controls.

Fail

A clickable div has aria-expanded but cannot be reached with Tab or activated with Space.

Fix guidance: Use a native button and keep aria-expanded synchronized with the visible panel.

Accordion

Pass

Each accordion header contains a button that updates aria-expanded and points to its panel.

Fail

Only the heading text is clickable and the state is not exposed to screen readers.

Fix guidance: The control must be the focusable button, not a non-interactive wrapper.

Modal dialog

Pass

The dialog has an accessible name, focus moves inside, Tab stays inside, Escape closes it, and focus returns to the opener.

Fail

A div has role="dialog" but background links remain in the Tab order.

Fix guidance: Dialog semantics are only one part. Focus management and background inertness are required.

Tabs

Pass

Arrow keys move between tabs, the active tab uses aria-selected="true", and each tab controls one tabpanel.

Fail

Tabs are built as links that do not update aria-selected or panel visibility.

Fix guidance: Tabs need a clear active state, correct relationships, and a predictable keyboard model.

Menu button

Pass

A button opens a short command menu, arrow keys move through menuitems, and Escape closes it.

Fail

A normal site navigation list uses role="menu" and breaks expected link navigation.

Fix guidance: Use nav and links for website navigation. Reserve menu roles for application-style commands.

Combobox

Pass

Typing filters suggestions, aria-expanded reflects popup state, and aria-activedescendant tracks the highlighted option.

Fail

A custom dropdown changes visually but screen readers only announce a plain text input.

Fix guidance: Comboboxes are complex. Prefer native select when it meets the interaction requirement.

Tooltip

Pass

A tooltip appears on focus and hover, is referenced with aria-describedby, and does not contain interactive controls.

Fail

A tooltip contains a required form instruction and disappears before keyboard users can read it.

Fix guidance: Tooltips are supplemental. Put required help text directly in the page.

Disabled controls

Pass

A true disabled button uses disabled. A focusable custom disabled item uses aria-disabled and blocks activation in code.

Fail

A link has aria-disabled="true" but still navigates when clicked.

Fix guidance: ARIA changes meaning, not behavior. You must also prevent the action.

Current navigation

Pass

The current page link uses aria-current="page".

Fail

Every link in the navigation uses aria-current or the active page is only shown by color.

Fix guidance: Use aria-current for exactly the current item in a set.

Live regions and alerts

Pass

A save confirmation uses role="status" and a validation failure uses role="alert" after submit.

Fail

The whole page is inside aria-live="assertive".

Fix guidance: Live regions should be small, purposeful, and used only for changes the user needs to hear.

Keyboard Expectations by Pattern

These expectations are useful when reviewing ARIA widgets. Exact behavior can vary, but these are common testing targets.

  • Disclosure: Tab reaches the button. Enter or Space toggles the panel. Focus remains predictable.
  • Accordion: Tab reaches each header button. Enter or Space expands and collapses. Arrow-key behavior is optional unless implemented consistently.
  • Modal dialog: focus moves into the dialog, Tab and Shift+Tab stay inside, Escape closes, and focus returns to the trigger.
  • Tabs: Tab enters the tablist, arrow keys move between tabs, Home and End may move to first or last tab, and the active tab controls the panel.
  • Menu button: Enter, Space, or Down Arrow opens the menu. Arrow keys move through items. Escape closes and returns focus.
  • Combobox: typing filters or edits, Down Arrow opens suggestions, Enter accepts a value, Escape closes, and focus remains clear.
  • Tooltip: focus and hover can reveal it. Escape should dismiss it. It must not trap focus or contain required interactive content.

ARIA Snippets

These snippets are safe starting points. Production components still need testing in your browser and assistive technology combinations.

Accessible disclosure

Use a real button and keep aria-expanded in sync with state.

const [open, setOpen] = useState(false);

<button
  type="button"
  aria-expanded={open}
  aria-controls="billing-panel"
  onClick={() => setOpen((value) => !value)}
>
  Billing details
</button>
<div id="billing-panel" hidden={!open}>
  Billing content goes here.
</div>

Accessible modal skeleton

This shows the required relationships. Add focus trap and focus return in the modal component.

function ConfirmDialog({ onClose }: { onClose: () => void }) {
  return (
    <div role="dialog" aria-modal="true" aria-labelledby="confirm-title">
      <h2 id="confirm-title">Delete report?</h2>
      <p>This action cannot be undone.</p>
      <button type="button" onClick={onClose}>Cancel</button>
      <button type="button">Delete report</button>
    </div>
  );
}

Tabs relationships

Each tab points to one panel and each panel points back to its tab.

<div role="tablist" aria-label="Report sections">
  <button id="tab-summary" role="tab" aria-selected="true" aria-controls="panel-summary">
    Summary
  </button>
  <button id="tab-issues" role="tab" aria-selected="false" aria-controls="panel-issues">
    Issues
  </button>
</div>
<section id="panel-summary" role="tabpanel" aria-labelledby="tab-summary">
  Summary content
</section>
<section id="panel-issues" role="tabpanel" aria-labelledby="tab-issues" hidden>
  Issues content
</section>

Menu button relationships

Use for command menus. Do not use role menu for ordinary website navigation.

<button type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="report-menu">
  Report actions
</button>
<ul id="report-menu" role="menu" hidden>
  <li role="none"><button role="menuitem" type="button">Export PDF</button></li>
  <li role="none"><button role="menuitem" type="button">Create Jira task</button></li>
</ul>

Form error description

Connect the error to the input and only set aria-invalid when the field is invalid.

<label htmlFor="email">Email</label>
<input
  id="email"
  type="email"
  aria-invalid={hasError}
  aria-describedby={hasError ? "email-error" : "email-help"}
/>
<p id="email-help">Use the address connected to your account.</p>
{hasError ? <p id="email-error">Enter a valid email address.</p> : null}

Current page navigation

Expose the current page state programmatically.

<nav aria-label="Primary">
  <a href="/">Home</a>
  <a href="/pricing" aria-current="page">Pricing</a>
  <a href="/accessibility">Learn Accessibility Testing</a>
</nav>

Polite status message

Use status for non-urgent changes like save confirmation.

<div role="status" aria-live="polite">
  Report copied to clipboard.
</div>

ARIA Anti-Patterns

When ARIA is wrong, it can make an interface harder to use than plain HTML.

  • Do not put role="button" on a div when a button element would work.
  • Do not add role="button" to a button element.
  • Do not use aria-hidden="true" on a container that includes focusable links, buttons, inputs, or iframes.
  • Do not use role="menu" for a normal website navigation menu.
  • Do not use aria-label with a different meaning than the visible label.
  • Do not use aria-disabled without blocking mouse, keyboard, and programmatic activation.
  • Do not add aria-live to large page regions that change often.
  • Do not use tabindex greater than 0 to force a custom focus order.