How to build an accessible MenuButton in React

The trigger is a real <button> exposing aria-haspopup="menu", aria-expanded reflecting open state, and aria-controls pointing at the role="menu" panel. ArrowDown/Enter/Space open the menu and move focus to the first enabled item, ArrowUp opens and focuses the last item, ArrowUp/ArrowDown rove focus between items with wrap-around, disabled items are aria-disabled and skipped by roving focus, Escape closes and restores focus to the trigger, and Tab closes without trapping focus. DismissableLayer provides outside-pointer and Escape dismissal, and high-contrast plus reduced-motion preferences are honored in CSS.

When to use a MenuButton

When not to use it

Keyboard interactions

Role menu, verified at WCAG 2.2-AA.

Common mistakes

Avoid: Rendering checkbox or radio state inside MenuButton items and treating selection as a chosen value

Prefer: Use Select or MultiSelect for value selection; MenuButton items are one-shot actions that close the menu on activation

role="menuitem" items do not model a selected value and dismiss the menu when activated, so they cannot represent persistent choices

Avoid: Leaving the trigger label empty with no visible text and no labels.open override

Prefer: Provide visible label text, or pass labels.open / rely on the built-in i18n accessible name

A trigger with no accessible name is unusable for screen-reader and voice-control users

Example

<MenuButton
  label="Actions"
  items={[
    { id: 'edit', label: 'Edit', onSelect: () => edit() },
    { id: 'duplicate', label: 'Duplicate', onSelect: () => duplicate() },
    { id: 'delete', label: 'Delete', onSelect: () => remove(), disabled: !canDelete },
  ]}
/>

See the full MenuButton reference →