Accessibility pattern · Buttons and actions

Menu button

The button says it opens a menu and whether the menu is open; inside, the arrow keys move between actions and one letter jumps to the action it starts. Choosing an action, pressing Escape or clicking away closes the menu, and focus goes back to the button that opened it.

WCAG criteria
7
Keyboard rules
8
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Q3 sales report.pdf

2.4 MB · Edited by Kavya

No action chosen yet

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or Space or Arrow DownOn the button, opens the menu and moves focus to the first action.
Arrow UpOn the button, opens the menu at the last action.
Arrow Down or Arrow UpIn the menu, moves to the next or previous action, wrapping at the ends.
Home or EndMoves to the first or last action.
A–ZMoves to the next action that starts with that letter; press D again to go from Duplicate to Download to Delete.
Enter or SpaceRuns the focused action, closes the menu and returns focus to the button.
EscapeCloses the menu and returns focus to the button. Tab, or a click outside, closes it too.
F2 or CtrlD or DeleteRuns Rename, Duplicate or Delete straight away while focus is on the card's button or in its menu.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the buttonActions, menu button, collapsed
Enter opens the menuActions, menu. Rename, menu item, F2, 1 of 5
Arrow Down moves onDuplicate, menu item, Control+D, 2 of 5
An action is chosenActions, menu button, collapsed. You chose Duplicate

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-menu-button" data-ap-menu-button>
  <div class="ap-menu-button__card">
    <div class="ap-menu-button__top">
      <span class="ap-menu-button__file" aria-hidden="true">
        <svg viewBox="0 0 24 24" focusable="false"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg>
      </span>
      <div class="ap-menu-button__about">
        <h3 class="ap-menu-button__name">Q3 sales report.pdf</h3>
        <p class="ap-menu-button__meta">2.4 MB · Edited by Kavya</p>
      </div>
      <div class="ap-menu-button__wrap">
        <button type="button" class="ap-menu-button__trigger" id="menu-button-trigger" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-button-menu">
          <span>Actions</span>
          <svg class="ap-menu-button__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
        </button>
        <div class="ap-menu-button__menu" role="menu" id="menu-button-menu" aria-labelledby="menu-button-trigger" hidden>
          <button type="button" class="ap-menu-button__item" role="menuitem" tabindex="-1" aria-keyshortcuts="F2" data-ap-action="rename">
            <svg class="ap-menu-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
            <span class="ap-menu-button__label">Rename</span>
            <kbd class="ap-menu-button__keys" aria-hidden="true">F2</kbd>
          </button>
          <button type="button" class="ap-menu-button__item" role="menuitem" tabindex="-1" aria-keyshortcuts="Control+D" data-ap-action="duplicate">
            <svg class="ap-menu-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
            <span class="ap-menu-button__label">Duplicate</span>
            <kbd class="ap-menu-button__keys" aria-hidden="true">Ctrl+D</kbd>
          </button>
          <button type="button" class="ap-menu-button__item" role="menuitem" tabindex="-1" data-ap-action="share">
            <svg class="ap-menu-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></svg>
            <span class="ap-menu-button__label">Share</span>
          </button>
          <button type="button" class="ap-menu-button__item" role="menuitem" tabindex="-1" data-ap-action="download">
            <svg class="ap-menu-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11"/><path d="m7 10 5 5 5-5"/><path d="M5 20h14"/></svg>
            <span class="ap-menu-button__label">Download</span>
          </button>
          <div class="ap-menu-button__sep" role="separator"></div>
          <button type="button" class="ap-menu-button__item ap-menu-button__item--danger" role="menuitem" tabindex="-1" aria-keyshortcuts="Delete" data-ap-action="delete">
            <svg class="ap-menu-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="m6 7 1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V4h6v3"/></svg>
            <span class="ap-menu-button__label">Delete</span>
            <kbd class="ap-menu-button__keys" aria-hidden="true">Del</kbd>
          </button>
        </div>
      </div>
    </div>

    <div class="ap-menu-button__preview" aria-hidden="true">
      <div class="ap-menu-button__page">
        <span class="ap-menu-button__line ap-menu-button__line--lead"></span>
        <span class="ap-menu-button__line"></span>
        <span class="ap-menu-button__line ap-menu-button__line--short"></span>
        <span class="ap-menu-button__bars"><span></span><span></span><span></span><span></span><span></span></span>
        <span class="ap-menu-button__line"></span>
        <span class="ap-menu-button__line ap-menu-button__line--short"></span>
      </div>
    </div>

    <p class="ap-menu-button__status" role="status" data-state="idle">
      <svg class="ap-menu-button__done" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
      <span data-ap-text>No action chosen yet</span>
    </p>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The menu, its items and the separator have roles, so the grouping and the destructive action's place at the end are exposed, not just drawn.

  • 1.4.1 Use of Color Level A

    Delete is set apart by a separator and a bin icon, not only by its red text.

  • 2.1.1 Keyboard Level A

    Opening, moving, choosing and closing all work from the keyboard, and the shortcuts shown in the menu really work.

  • 2.4.3 Focus Order Level A

    Focus moves into the menu when it opens and back to the button when it closes, so it is never lost.

  • 2.4.7 Focus Visible Level AA

    The focused action is marked with a fill and a ring, and the button shows a ring when it has focus.

  • 4.1.2 Name, Role, Value Level A

    aria-haspopup and aria-expanded say the button opens a menu and whether it is open; each action is a menuitem with aria-keyshortcuts.

  • 4.1.3 Status Messages Level AA

    The result of an action is announced in a status message after focus returns to the button.

Usage

When to use it

Use it

  • A handful of actions on one object, like a file or a row, that would crowd the screen as separate buttons.
  • Actions people use now and then, where a click to reveal them is a fair price for the space saved.

Use something else

  • Navigation between pages: use a disclosure with a list of links, not role=menu.
  • Choosing a value for a form field: use a select or a listbox.
  • One or two actions: show them as buttons.

Common failures

How it usually goes wrong

  • role=menu for site navigation

    A menu tells screen readers to expect application keys, not links. For a list of pages, a button that shows and hides a list of links is the right pattern.

  • Focus left on the button

    If the menu opens but focus stays behind, arrow keys scroll the page instead. Focus moves to the first action when the menu opens.

  • Every item in the Tab order

    Tabbing through ten menu items is slow, and Tab is how people leave a menu. Items here are reached with arrow keys; Tab closes the menu.

  • Focus lost when the menu closes

    Hiding the menu while an item has focus drops focus to the top of the page. Escape, an action or a click outside sends it back to the button.

  • Shortcut hints that do nothing

    Showing Ctrl+D beside an action promises it works. The shortcuts here run the action, and aria-keyshortcuts exposes them to assistive technology.

  • A danger item mixed in with the rest

    Delete in the middle of the list, distinguished only by color, is easy to hit by mistake. It sits last, after a separator, with its own icon.

Notes

Building it

  • Keep the menu's items as buttons with role=menuitem and tabindex=-1: Enter and Space press them natively, and only the arrow keys move between them.
  • The menu is named by its button with aria-labelledby, so opening it announces "Actions, menu".
  • The shortcut hints are aria-hidden because aria-keyshortcuts already exposes them; without that attribute, leave the hints readable.
  • Moving the mouse over an action focuses it, so a single highlight follows both the pointer and the keyboard.
  • Typeahead compares the visible label, so it keeps working when the page is translated.

Sources: WAI-ARIA Authoring Practices: Menu Button · WAI-ARIA Authoring Practices: Menu and Menubar

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction