Skip to content

Find what you need, instantly

Search components, ARIA roles & attributes, and WCAG criteria.

OverlaysFlagship guide

Menu & Menu Button

A button that opens a menu of actions or links, navigable with arrow keys.

Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.

Implementation

menu-button-pattern.tsx
function MenuButtonPattern() {
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const buttonRef = useRef(null);
  const itemRefs = useRef([]);

  function openMenu(focusIndex) {
    setOpen(true);
    setActiveIndex(focusIndex);
  }
  function closeMenu(restoreFocus) {
    setOpen(false);
    if (restoreFocus) buttonRef.current?.focus();
  }

  useEffect(() => {
    if (open) itemRefs.current[activeIndex]?.focus();
  }, [open, activeIndex]);

  function onMenuKeyDown(e) {
    if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex(i => (i + 1) % ITEMS.length); }
    if (e.key === "ArrowUp")   { e.preventDefault(); setActiveIndex(i => (i - 1 + ITEMS.length) % ITEMS.length); }
    if (e.key === "Escape")    { e.preventDefault(); closeMenu(true); }
    // Menus aren't modal — Tab closes and continues to the next element.
    if (e.key === "Tab") closeMenu(false);
    if (e.key === "Enter" || e.key === " ") { e.preventDefault(); closeMenu(true); }
  }

  return (
    <>
      <button
        ref={buttonRef}
        aria-haspopup="menu"
        aria-expanded={open}
        aria-controls="actions-menu"
        onClick={() => (open ? closeMenu(false) : openMenu(0))}
        onKeyDown={e => {
          if (e.key === "ArrowDown" || e.key === "Enter") { e.preventDefault(); openMenu(0); }
          if (e.key === "ArrowUp") { e.preventDefault(); openMenu(ITEMS.length - 1); }
        }}
      >
        Actions
      </button>
      {open && (
        <ul id="actions-menu" role="menu" aria-label="Actions" onKeyDown={onMenuKeyDown}>
          {ITEMS.map((item, i) => (
            <li key={item} role="none">
              <button
                ref={el => (itemRefs.current[i] = el)}
                role="menuitem"
                tabIndex={-1}
                onClick={() => closeMenu(true)}
              >
                {item}
              </button>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

Live demo

Required roles, states & properties

Required roles, states, and properties
ElementAttributeWhy
Trigger <button>aria-haspopup="menu"Tells AT this button opens a menu, so it can announce "has popup, menu" before it's even activated.
Trigger <button>aria-expandedCommunicates the menu's open/closed state on the trigger itself.
Trigger <button>aria-controlsAssociates the trigger with the menu it opens, by id.
Menu containerrole="menu"Identifies the popup as a menu (not a generic list), enabling menu-specific AT navigation and the "menu" role announcement.
Each itemrole="menuitem", tabIndex="-1"Identifies each row as an actionable menu item; tabIndex=-1 keeps items out of the page Tab order — they're reached only via the menu's own arrow-key roving focus.

Keyboard interaction model

Keyboard interaction model
KeyBehavior
Enter / Space / Down Arrow (on button)Opens the menu and moves focus to the first item.
Up Arrow (on button)Opens the menu and moves focus to the last item.
Down / Up Arrow (in menu)Moves focus to the next / previous item, wrapping at the ends.
Home / EndMoves focus to the first / last item.
Enter / Space (on item)Activates the item and closes the menu, returning focus to the button.
EscapeCloses the menu and returns focus to the button.
TabCloses the menu (it is not modal) and moves focus to the next focusable element on the page.

Focus management rules

  • Opening via Down Arrow focuses the first item; via Up Arrow focuses the last item.
  • Menu items use a roving tabindex (all -1) — arrow keys move real focus between them directly.
  • Escape or activating an item closes the menu and restores focus to the trigger button.
  • Tab closes the menu without restoring focus to the trigger — it continues the page's natural tab sequence, since menus are not modal.

WCAG 2.2 success criteria mapping

WCAG 2.2 success criteria that apply to this component
SCNameLevelWhy it applies
2.1.1KeyboardAAll functionality is operable through a keyboard interface with no specific timing.
2.1.2No Keyboard TrapAKeyboard focus can always be moved away from a component using standard navigation.
2.4.3Focus OrderAFocusable components receive focus in an order that preserves meaning and operability.
4.1.2Name, Role, ValueAFor all UI components, name, role, and value are programmatically determinable; states and changes are announced.

References