Accessibility pattern · Buttons and actions

Context menu

Right-click, Shift+F10 and the Menu key all open the same role=menu at the item, with focus on its first action. A More actions button on every row opens it as well, so nobody depends on a right-click or a key they have never heard of.

WCAG criteria
6
Keyboard rules
6
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.

Project files

4 files

Right-click a file or press Shift+F10 for its actions.

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
ShiftF10 or MenuOn a file, opens its menu at the file with focus on the first action.
Enter or SpaceOn More actions, opens the menu; in the menu, runs the focused action and returns focus to where the menu was opened.
Arrow Down or Arrow UpMoves to the next or previous action, wrapping at the ends. On More actions, opens the menu at the first or last action.
Home or EndMoves to the first or last action.
A–ZMoves to the next action that starts with that letter.
Escape or TabCloses the menu without acting and returns focus to the file or button that opened it.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches a fileQ3 budget.xlsx, button, 48 KB · Edited today. Right-click a file or press Shift+F10 for its actions.
Shift+F10 opens the menuQ3 budget.xlsx, menu. Preview, menu item, 1 of 5
Focus reaches a More actions buttonMore actions Site plan.pdf, menu button, collapsed
Rename is chosenQ3 budget.xlsx, button. Chosen: Rename Q3 budget.xlsx

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-context-menu" data-ap-context-menu>
  <div class="ap-context-menu__top">
    <h3 class="ap-context-menu__label" id="context-menu-files">Project files</h3>
    <p class="ap-context-menu__count">4 files</p>
  </div>
  <p class="ap-context-menu__hint" id="context-menu-hint">Right-click a file or press <kbd>Shift</kbd>+<kbd>F10</kbd> for its actions.</p>

  <ul class="ap-context-menu__list" aria-labelledby="context-menu-files">
    <li class="ap-context-menu__row" data-ap-file>
      <button type="button" class="ap-context-menu__item" data-ap-item aria-labelledby="context-menu-name-1" aria-describedby="context-menu-meta-1 context-menu-hint">
        <span class="ap-context-menu__type ap-context-menu__type--sheet" aria-hidden="true" translate="no">XLS</span>
        <span class="ap-context-menu__text">
          <span class="ap-context-menu__file" id="context-menu-name-1" translate="no">Q3 budget.xlsx</span>
          <span class="ap-context-menu__meta" id="context-menu-meta-1">48 KB · Edited today</span>
        </span>
      </button>
      <button type="button" class="ap-context-menu__more" data-ap-more aria-haspopup="menu" aria-expanded="false" aria-controls="context-menu-menu" aria-labelledby="context-menu-more context-menu-name-1">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="5" cy="12" r="1.75"/><circle cx="12" cy="12" r="1.75"/><circle cx="19" cy="12" r="1.75"/></svg>
      </button>
    </li>
    <li class="ap-context-menu__row" data-ap-file>
      <button type="button" class="ap-context-menu__item" data-ap-item aria-labelledby="context-menu-name-2" aria-describedby="context-menu-meta-2 context-menu-hint">
        <span class="ap-context-menu__type ap-context-menu__type--pdf" aria-hidden="true" translate="no">PDF</span>
        <span class="ap-context-menu__text">
          <span class="ap-context-menu__file" id="context-menu-name-2" translate="no">Site plan.pdf</span>
          <span class="ap-context-menu__meta" id="context-menu-meta-2">2.4 MB · Edited yesterday</span>
        </span>
      </button>
      <button type="button" class="ap-context-menu__more" data-ap-more aria-haspopup="menu" aria-expanded="false" aria-controls="context-menu-menu" aria-labelledby="context-menu-more context-menu-name-2">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="5" cy="12" r="1.75"/><circle cx="12" cy="12" r="1.75"/><circle cx="19" cy="12" r="1.75"/></svg>
      </button>
    </li>
    <li class="ap-context-menu__row" data-ap-file>
      <button type="button" class="ap-context-menu__item" data-ap-item aria-labelledby="context-menu-name-3" aria-describedby="context-menu-meta-3 context-menu-hint">
        <span class="ap-context-menu__type ap-context-menu__type--doc" aria-hidden="true" translate="no">DOC</span>
        <span class="ap-context-menu__text">
          <span class="ap-context-menu__file" id="context-menu-name-3" translate="no">Launch notes.docx</span>
          <span class="ap-context-menu__meta" id="context-menu-meta-3">16 KB · Edited 2 Oct</span>
        </span>
      </button>
      <button type="button" class="ap-context-menu__more" data-ap-more aria-haspopup="menu" aria-expanded="false" aria-controls="context-menu-menu" aria-labelledby="context-menu-more context-menu-name-3">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="5" cy="12" r="1.75"/><circle cx="12" cy="12" r="1.75"/><circle cx="19" cy="12" r="1.75"/></svg>
      </button>
    </li>
    <li class="ap-context-menu__row" data-ap-file>
      <button type="button" class="ap-context-menu__item" data-ap-item aria-labelledby="context-menu-name-4" aria-describedby="context-menu-meta-4 context-menu-hint">
        <span class="ap-context-menu__type ap-context-menu__type--img" aria-hidden="true" translate="no">JPG</span>
        <span class="ap-context-menu__text">
          <span class="ap-context-menu__file" id="context-menu-name-4" translate="no">Team offsite.jpg</span>
          <span class="ap-context-menu__meta" id="context-menu-meta-4">3.1 MB · Edited 28 Sep</span>
        </span>
      </button>
      <button type="button" class="ap-context-menu__more" data-ap-more aria-haspopup="menu" aria-expanded="false" aria-controls="context-menu-menu" aria-labelledby="context-menu-more context-menu-name-4">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="5" cy="12" r="1.75"/><circle cx="12" cy="12" r="1.75"/><circle cx="19" cy="12" r="1.75"/></svg>
      </button>
    </li>
  </ul>
  <span id="context-menu-more" hidden>More actions</span>

  <div class="ap-context-menu__foot">
    <svg class="ap-context-menu__foot-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 11.5 11.5 14 16 9.5"/><circle cx="12" cy="12" r="9"/></svg>
    <p class="ap-context-menu__status" role="status" aria-atomic="true">No action chosen yet</p>
  </div>

  <div class="ap-context-menu__menu" id="context-menu-menu" role="menu" popover="manual" aria-labelledby="context-menu-name-1">
    <button type="button" class="ap-context-menu__action" role="menuitem" tabindex="-1">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="3"/></svg>
      <span data-ap-label>Preview</span>
    </button>
    <button type="button" class="ap-context-menu__action" role="menuitem" tabindex="-1">
      <svg 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 data-ap-label>Rename</span>
    </button>
    <button type="button" class="ap-context-menu__action" role="menuitem" tabindex="-1">
      <svg 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 data-ap-label>Duplicate</span>
    </button>
    <button type="button" class="ap-context-menu__action" role="menuitem" tabindex="-1">
      <svg 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 data-ap-label>Download</span>
    </button>
    <div class="ap-context-menu__sep" role="separator"></div>
    <button type="button" class="ap-context-menu__action ap-context-menu__action--danger" role="menuitem" tabindex="-1">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16"/><path d="M9 7V4h6v3"/><path d="M6.5 7l1 13h9l1-13"/><path d="M10 11v5M14 11v5"/></svg>
      <span data-ap-label>Delete</span>
    </button>
  </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 is named after the file it acts on, and a separator sets Delete apart from the safer actions.

  • 2.1.1 Keyboard Level A

    Everything a right-click opens, Shift+F10, the Menu key and the More actions button open too; inside, arrows, letters, Enter and Escape do the rest.

  • 2.4.3 Focus Order Level A

    Focus moves into the menu when it opens and back to the file or button that opened it, however it closes.

  • 2.4.7 Focus Visible Level AA

    The focused action is filled and ringed, and every file and button shows a ring when it has keyboard focus.

  • 4.1.2 Name, Role, Value Level A

    Actions are menu items in a role=menu with a name; the More actions button says it opens a menu and whether it is expanded.

  • 4.1.3 Status Messages Level AA

    The chosen action is reported in a status message that is announced without moving focus.

Usage

When to use it

Use it

  • Quick access to actions on one item in a list, a table or a canvas, for people who expect a right-click.
  • Apps where a visible button already offers the same actions, so the menu is a shortcut, not the only way.

Use something else

  • As the only way to reach an action: a right-click menu is invisible, and many people never try one.
  • Over ordinary links and text: replacing the browser's own menu takes away Open in new tab and Copy.
  • Choosing a value, like a sort order: use a select or a radio group.

Common failures

How it usually goes wrong

  • Right-click as the only way in

    Touch screens, keyboard users and many mouse users never find a right-click menu. A visible More actions button opens the same menu here.

  • No keyboard way to open it

    Without Shift+F10 and the Menu key, keyboard users cannot reach the menu from the item at all. Both open it here, at the focused file.

  • Focus left behind on the page

    If focus stays on the item while the menu opens, the arrow keys scroll the page instead of moving through actions. Focus moves to the first action.

  • Focus dropped on close

    After an action or Escape, focus has to go back to the item. Losing it to the top of the page means finding your place again.

  • role=menu on navigation links

    A menu promises arrow keys and menu items. Site links wrapped in role=menu lose their link behaviour; a plain list of links is the right markup.

  • Blocking the browser's menu everywhere

    Cancelling every contextmenu event takes Copy and Open in new tab away from text and links too. Only the file rows open this menu.

Notes

Building it

  • One menu serves every row: the script names it after the file with aria-labelledby and places it at the pointer, or under the file when opened from the keyboard.
  • The menu is a manual popover, so it sits in the top layer where no overflow can clip it; the script closes it on Escape, Tab, a click outside or a chosen action.
  • Some browsers fire contextmenu for Shift+F10 and the Menu key and some do not, so the script handles those keys itself and ignores the duplicate event.
  • A page-wide custom right-click menu has to step aside over the rows, or it takes the right-click and the Menu key before this menu sees them.
  • The menu items are buttons with role=menuitem and tabindex=-1: the arrow keys move between them, Tab never does, and hovering one focuses it so the highlight and focus agree.

Sources: WAI-ARIA Authoring Practices: Menu and Menubar · WAI-ARIA Authoring Practices: Menu Button · HTML: the popover attribute

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