ESC

Type to search the knowledge base.

Accessible Menus Pattern

APG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.

advanced3 min read
  • accessibility
  • accessible-menus

“Menu” in UI kits often means a dropdown of actions. In ARIA, menu / menuitem imply a specific keyboard interaction model (arrow keys, not just Tab through every item). Misusing role="menu" on site navigation is a common antipattern.

Docs: APG Menu, APG Menubar, Navigation menus.

UI Prefer
Site primary nav links <nav> + links (and disclosure for mobile)
Application action menu (Edit, Delete) menu pattern
Simple show/hide extra links Disclosure button + aria-expanded

Don’t put role="menu" on a list of <a href> site links — screen reader users expect navigation landmarks, not application menus.

Action menu skeleton

<button
  type="button"
  id="actions-btn"
  aria-haspopup="menu"
  aria-expanded="false"
  aria-controls="actions-menu"
>
  Actions
</button>
<ul id="actions-menu" role="menu" hidden aria-labelledby="actions-btn">
  <li role="presentation">
    <button type="button" role="menuitem">Edit</button>
  </li>
  <li role="presentation">
    <button type="button" role="menuitem">Duplicate</button>
  </li>
  <li role="presentation">
    <button type="button" role="menuitem">Delete</button>
  </li>
</ul>

Keyboard expectations (application menu)

Key Behavior
Enter/Space on button Open menu; focus first item
Down/Up Move between menuitems
Home/End First/last
Escape Close; return focus to button
Tab Typically closes and moves focus (per APG)
Character Typeahead optional

Use roving tabindex: only the active menuitem has tabindex="0", others -1.

Focus management

  1. Store opener.
  2. Open → focus first item.
  3. Close → focus opener.

See focus management.

Menubar + submenu patterns add Right/Left arrows. Complexity climbs fast — use a library or follow APG meticulously.

Footguns

  1. role="menu" on site nav.
  2. Mouse hover open only.
  3. Focus lost on close.
  4. div menuitems without keyboard activation.
  5. Missing aria-expanded on the opener.

Interview out-loud answer

“I distinguish navigation links from application menus. True menus use arrow-key navigation, aria-expanded on the button, and restore focus on Escape. For simple nav, I use links and disclosures instead of role=menu.”

Roving tabindex sketch

const items = [...menu.querySelectorAll('[role="menuitem"]')];
let index = 0;

function setActive(i) {
  items.forEach((el, j) => {
    el.tabIndex = j === i ? 0 : -1;
  });
  items[i].focus();
  index = i;
}

menu.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown') {
    e.preventDefault();
    setActive((index + 1) % items.length);
  }
  if (e.key === 'ArrowUp') {
    e.preventDefault();
    setActive((index - 1 + items.length) % items.length);
  }
  if (e.key === 'Escape') {
    closeMenu();
    opener.focus();
  }
});

Separators and disabled items

<li role="separator"></li>
<button type="button" role="menuitem" aria-disabled="true">Rename</button>

Disabled items must not activate. Pick one APG behavior for whether arrows land on them and stick to it.

Outside click and focus

Mouse users expect click-outside to close. Restore focus to the opener on Escape always; on outside click, restoring to the opener is usually still correct so keyboard users aren’t stranded.

Mobile and touch

Application menus on touch devices often open on press and close on outside tap. Keep the same ARIA state updates (aria-expanded) as keyboard. Ensure hit targets meet size guidance so menuitems aren’t tiny text-only rows.

Further reading

Related guides