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.
<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.
| 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.
| 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 themtabindex="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.