Menu button

A menu button is a button that opens a menu — a short list of actions the user can run on something: Edit, Duplicate, Delete. It is the WAI-ARIA Authoring Practices “Menu Button” pattern, built from a <button> with aria-haspopup and a role="menu" list whose children are role="menuitem".

Use it only for commands. A role="menu" tells assistive technology “this is a set of actions, navigate it with the arrow keys.” That is the wrong promise for a list of links to other pages. For site or in-page navigation, do not use this pattern — use a plain list of links (<nav> with <ul>/<a>), optionally revealed by a disclosure button. Reserve role="menu" for menus of actions, the way a desktop application’s menu bar works.

Live demo

The button below is a real, working menu button. Open it with the mouse or, while it has focus, with Enter, Space, or ( opens it on the last item). Move with the arrow keys, choose with Enter, and the chosen action is announced below.

When to use

Actions, not navigation

Reach for a menu button when a control needs a compact set of actions that act on the current object or context — a row’s “⋯ More” menu of Edit / Duplicate / Delete, a toolbar’s Export as…, an account button’s Sign out.

Do not use role="menu" for a list of links to other pages or sections — a primary nav, a “Products” dropdown, a sitemap. Those are navigation. Use a normal <nav> with a list of links, revealed (if it collapses) by a disclosure button with aria-expanded. The menu roles add keyboard semantics that confuse screen-reader users when they’re applied to plain links.

Markup

The enhanced markup. The list ships hidden; the script shows and hides it and keeps aria-expanded in sync. Every menuitem carries tabindex="-1" — they are reached by arrow keys, not Tab (roving tabindex). The optional aria-live region announces the chosen action.

menu-button.html
<div class="ewa-menu" data-menu-button>
  <button class="ewa-btn ewa-btn--secondary ewa-menu__trigger" type="button"
          aria-haspopup="true" aria-expanded="false" aria-controls="demo-menu">
    Actions
  </button>
  <ul class="ewa-menu__list" id="demo-menu" role="menu" hidden>
    <li class="ewa-menu__item" role="menuitem" tabindex="-1">Edit</li>
    <li class="ewa-menu__item" role="menuitem" tabindex="-1">Duplicate</li>
    <li class="ewa-menu__item" role="menuitem" tabindex="-1">Delete</li>
  </ul>

  <!-- Optional: announces the chosen action to screen readers -->
  <p class="visually-hidden" data-menu-live aria-live="polite"></p>
</div>

Keyboard interactions

The full keyboard contract, split between the trigger button and the open menu.

Keyboard interactions
Focus is on Key Result
The button Enter / Space / Open the menu and move focus to the first item.
The button Open the menu and move focus to the last item.
A menu item / Move focus to the next / previous item; wraps at the ends.
A menu item Home / End Move focus to the first / last item.
A menu item Enter / Space Activate the item, close the menu, and return focus to the button.
A menu item Esc Close the menu and return focus to the button.
A menu item Tab Close the menu; focus moves on to the next control as usual.

ARIA roles, states, and properties

What each attribute does and where it goes. The script keeps the one stateful attribute — aria-expanded — in sync with the visible state.

ARIA contract
On Attribute Purpose
Button aria-haspopup="true" Announces that the button opens a popup. "true" is treated as "menu"; aria-haspopup="menu" is equally valid and explicit.
Button aria-expanded "false" when the menu is closed, "true" when open. The script toggles it on every open/close.
Button aria-controls Points to the id of the menu the button controls.
List role="menu" Marks the container as a menu of commands, so AT offers menu navigation.
Each item role="menuitem" Marks each child as an actionable command within the menu.
Each item tabindex="-1" Roving tabindex: items are focusable by script but kept out of the page Tab order. Only the active item is .focus()’d.

Common mistakes

Pitfalls to avoid

  • Using role="menu" for navigation. A list of links to other pages is not an actions menu. The menu roles tell screen-reader users to expect commands and arrow-key navigation; on plain links they mislead. Use a <nav> + list of links, optionally behind a disclosure button.
  • Not restoring focus. When the menu closes — on Esc, on activating an item, on Tab — focus must go somewhere predictable. On Esc and on activation, return it to the button. Leaving focus on a now-hidden item strands keyboard and screen-reader users.
  • Not closing on Esc or outside click. An open menu must close on Esc (focus back to the button) and on a click outside it. A menu you can’t dismiss traps the user.
  • Leaving menu items in the tab order. Items must be tabindex="-1" and reached with the arrow keys (roving tabindex). Giving them tabindex="0" floods the Tab sequence and breaks the expected menu interaction.
  • Forgetting aria-expanded. If it never flips to "true", screen-reader users can’t tell the menu is open. Keep it in sync with the visible state.