Navigation

Menu Button

Available

A button that opens a menu on activation. Uses aria-haspopup and aria-expanded to convey the relationship.

What Is a Menu Button?

A menu button is a button that, when pressed, pops up a list of actions (a menu). Think of a "⋯" or "Actions ▾" button that reveals options like "Duplicate, Rename, Delete." It lets you provide a collection of commands without taking up extra space.

Note

The "menu" here refers to application commands. For site navigation, you typically don't use role="menu". Instead, use <nav> with a list of links (see the Menubar page for details).

Why Does Accessibility Matter?

Live Demo (Recommended Implementation)

The menu button below follows the APG pattern. Try operating it without a mouse.

Accessible Menu Button

Try it: Tab to the button → Enter / Space / ↓ to open (focus moves to the first item) → ↑ ↓ to navigate items, Home / End to jump to edges → Enter to execute → Esc to close and return to the button.

Tip

Focus management during open/close is key. Move focus to the first item when opened, and return focus to the button when closed with Esc, so keyboard users never lose track of where they are.

Keyboard Interaction

KeyActionPriority
Enter / Space / Down Arrow (on the button)Open the menu and move focus to the first itemRequired
Up Arrow (on the button)Open the menu and move focus to the last itemOptional
Up / Down Arrow (within the menu)Move focus to the previous / next itemRequired
Home / EndMove to the first / last itemRecommended
Enter / Space (on an item)Activate the item, close the menu, and return focus to the buttonRequired
EscClose the menu and return focus to the buttonRequired

Required WAI-ARIA Roles and Properties

TargetAttribute / RoleMeaning
Trigger<button> + aria-haspopup="menu"Conveys that this is a button that opens a menu when pressed.
Triggeraria-expanded="true | false"Indicates whether the menu is open. Must be updated whenever the menu opens or closes.
Triggeraria-controls="menu-id"Identifies which menu the button opens.
Menu containerrole="menu" + aria-labelledbyConveys that this is a menu and that it is labeled by the trigger.
Each itemrole="menuitem" + tabindex="-1"Conveys that this is a menu item. Focus is managed via JavaScript (roving tabindex).
Closed menuhiddenRemoves the menu from screen reader announcements and interaction while closed.

Implementation: Recommended Pattern (Good)

Good / Recommended

The trigger uses <button aria-haspopup="menu" aria-expanded>, and the menu usesrole="menu" + role="menuitem". Focus management on open/close and Esc return are implemented in JS.

Markup:

<div class="menu">
  <button type="button"
          id="menu-button-trigger"
          aria-haspopup="menu"
          aria-expanded="false"
          aria-controls="menu-button-list">
    Actions ▾
  </button>

  <ul id="menu-button-list"
      role="menu"
      aria-labelledby="menu-button-trigger"
      hidden>
    <li role="menuitem" tabindex="-1">Duplicate</li>
    <li role="menuitem" tabindex="-1">Rename</li>
    <li role="menuitem" tabindex="-1">Delete</li>
  </ul>
</div>

Open/close and focus management script:

const trigger = document.getElementById('menu-button-trigger');
const menu = document.getElementById('menu-button-list');
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));

function open(focusIndex) {
  menu.hidden = false;
  trigger.setAttribute('aria-expanded', 'true');
  items[focusIndex].focus(); // Move focus into the menu when opened
}
function close(returnFocus = true) {
  menu.hidden = true;
  trigger.setAttribute('aria-expanded', 'false');
  if (returnFocus) trigger.focus(); // Return focus to trigger on Esc, etc.
}

trigger.addEventListener('click', () =>
  menu.hidden ? open(0) : close());
trigger.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
    e.preventDefault(); open(0);
  } else if (e.key === 'ArrowUp') {
    e.preventDefault(); open(items.length - 1);
  }
});

items.forEach((item, i) => {
  item.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length].focus(); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); }
    else if (e.key === 'Home') { e.preventDefault(); items[0].focus(); }
    else if (e.key === 'End') { e.preventDefault(); items[items.length - 1].focus(); }
    else if (e.key === 'Escape') { close(); }
    else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); run(item); }
  });
  item.addEventListener('click', () => run(item));
});

function run(item) { /* Execute action */ close(); }

// Close the menu when clicking outside
document.addEventListener('click', (e) => {
  if (!menu.hidden && !e.target.closest('.menu')) close(false);
});

Anti-pattern (Bad)

Below is a dropdown that only opens on :hover.It opens when you hover with a mouse, but it's completely inoperable with a keyboard.

Hover-only dropdown

Try it: Hovering opens it, but you can't focus the trigger with Tab, and neither ↓ nor Esc work. Moving the mouse away closes it immediately.

<!-- ❌ Anti-pattern: dropdown that only opens on hover -->
<div class="dropdown">
  <div class="trigger">Actions ▾</div>
  <!-- Only shown via CSS while hovering. No ARIA, no keyboard support -->
  <ul class="menu-list">
    <li onclick="run('copy')">Duplicate</li>
    <li onclick="run('rename')">Rename</li>
    <li onclick="run('delete')">Delete</li>
  </ul>
</div>

<style>
  .menu-list { display: none; }
  .dropdown:hover .menu-list { display: block; } /* hover-dependent */
</style>

Bad / Avoid

Problems with this implementation:

  • Cannot be opened with a keyboard — The div trigger is not focusable. /Enter have no effect.
  • Hover-dependent — Breaks for people who can't use a mouse and in touch environments. Closes as soon as the mouse moves away.
  • Role and state are not communicated — No aria-haspopup/aria-expanded/role="menu".
  • Cannot close with Esc and no focus management — Users lose track of where they are.

Implementation Checklist


Source (English):Menu Button Pattern — W3C APG(opens in a new tab)