useMenu
Provides the behavior and accessibility implementation for a menu component. A menu displays a list of actions or options that a user can choose.
| install | yarn add @react-aria/menu | 
|---|---|
| version | 3.1.3 | 
| usage | import {useMenu, useMenuItem, useMenuSection} from '@react-aria/menu' | 
API#
useMenu<T>(
  props: AriaMenuOptions<T>,
  state: TreeState<T>,
  ref: RefObject<HTMLElement>
): MenuAriauseMenuItem<T>(
  props: AriaMenuItemProps,
  state: TreeState<T>,
  ref: RefObject<HTMLElement>
): MenuItemAriauseMenuSection(
  (props: AriaMenuSectionProps
)): MenuSectionAriaFeatures#
There is no native element to implement a menu in HTML that is widely supported. useMenu
helps achieve accessible menu components that can be styled as needed.
Note: useMenu only handles the menu itself. For a dropdown menu, combine with useMenuTrigger.
- Exposed to assistive technology as a menuwithmenuitemchildren using ARIA
- Support for single, multiple, or no selection
- Support for disabled items
- Support for sections
- Complex item labeling support for accessibility
- Support for mouse, touch, and keyboard interactions
- Tab stop focus management
- Keyboard navigation support including arrow keys, home/end, page up/down
- Automatic scrolling support during keyboard navigation
- Typeahead to allow focusing items by typing text
- Virtualized scrolling support for performance with long lists
Anatomy#
A menu consists of a container element, with a list of menu items or groups inside.
useMenu, useMenuItem, and useMenuSection handle exposing this to assistive
technology using ARIA, along with handling keyboard, mouse, and interactions to support
selection and focus behavior.
useMenu returns props that you should spread onto the menu container element:
| Name | Type | Description | 
| menuProps | HTMLAttributes<HTMLElement> | Props for the menu element. | 
useMenuItem returns props for an individual item and its children:
| Name | Type | Description | 
| menuItemProps | HTMLAttributes<HTMLElement> | Props for the menu item element. | 
| labelProps | HTMLAttributes<HTMLElement> | Props for the main text element inside the menu item. | 
| descriptionProps | HTMLAttributes<HTMLElement> | Props for the description text element inside the menu item, if any. | 
| keyboardShortcutProps | HTMLAttributes<HTMLElement> | Props for the keyboard shortcut text element inside the item, if any. | 
useMenuSection returns props for a section:
| Name | Type | Description | 
| itemProps | HTMLAttributes<HTMLElement> | Props for the wrapper list item. | 
| headingProps | HTMLAttributes<HTMLElement> | Props for the heading element, if any. | 
| groupProps | HTMLAttributes<HTMLElement> | Props for the heading element, if any. | 
State is managed by the useTreeState
hook from @react-stately/tree. The state object should be passed as an option
to each of the above hooks.
If a menu, menu item, or group does not have a visible label, an aria-label or aria-labelledby
prop must be passed instead to identify the element to assistive technology.
State management#
useMenu requires knowledge of the items in the menu in order to handle keyboard
navigation and other interactions. It does this using the Collection
interface, which is a generic interface to access sequential unique keyed data. You can
implement this interface yourself, e.g. by using a prop to pass a list of item objects,
but useTreeState from
@react-stately/tree implements a JSX based interface for building collections instead.
See Collection Components for more information,
and Collection Interface for internal details.
In addition, useTreeState
manages the state necessary for multiple selection and exposes
a SelectionManager,
which makes use of the collection to provide an interface to update the selection state.
For more information, see Selection.
Example#
This example uses HTML <ul> and <li> elements to represent the menu, and applies
props from useMenu
and useMenuItem.
import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';
function Menu(props) {
  // Create state based on the incoming props
  let state = useTreeState({...props selectionMode: 'none'});
  // Get props for the menu element
  let ref = ReactuseRef();
  let {menuProps} = useMenu(props state ref);
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        padding: 0
        listStyle: 'none'
        border: '1px solid gray'
        maxWidth: 250
      }>
      [...statecollection]map((item) => (
        <MenuItem
          key=itemkey
          item=item
          state=state
          onAction=propsonAction
        />
      ))
    </ul>
  );
}
function MenuItem({item state onAction}) {
  // Get props for the menu item element
  let ref = ReactuseRef();
  let {menuItemProps} = useMenuItem(
    {
      key: itemkey
      isDisabled: itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused setFocused] = ReactuseState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});
  return (
    <li
      ...mergeProps(menuItemProps focusProps)
      ref=ref
      style={
        background: isFocused ? 'gray' : 'transparent'
        color: isFocused ? 'white' : null
        padding: '2px 5px'
        outline: 'none'
        cursor: 'pointer'
      }>
      itemrendered
    </li>
  );
}
<Menu onAction=alert aria-label="Actions">
  <Item key="one">One</Item>
  <Item key="two">Two</Item>
  <Item key="three">Three</Item>
</Menu>import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';
function Menu(props) {
  // Create state based on the incoming props
  let state = useTreeState({
    ...props
    selectionMode: 'none'
  });
  // Get props for the menu element
  let ref = ReactuseRef();
  let {menuProps} = useMenu(props state ref);
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        padding: 0
        listStyle: 'none'
        border: '1px solid gray'
        maxWidth: 250
      }>
      [...statecollection]map((item) => (
        <MenuItem
          key=itemkey
          item=item
          state=state
          onAction=propsonAction
        />
      ))
    </ul>
  );
}
function MenuItem({item state onAction}) {
  // Get props for the menu item element
  let ref = ReactuseRef();
  let {menuItemProps} = useMenuItem(
    {
      key: itemkey
      isDisabled: itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused setFocused] = ReactuseState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});
  return (
    <li
      ...mergeProps(menuItemProps focusProps)
      ref=ref
      style={
        background: isFocused ? 'gray' : 'transparent'
        color: isFocused ? 'white' : null
        padding: '2px 5px'
        outline: 'none'
        cursor: 'pointer'
      }>
      itemrendered
    </li>
  );
}
<Menu onAction=alert aria-label="Actions">
  <Item key="one">One</Item>
  <Item key="two">Two</Item>
  <Item key="three">Three</Item>
</Menu>import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';
function Menu(props) {
  // Create state based on the incoming props
  let state = useTreeState(
    {
      ...props
      selectionMode:
        'none'
    }
  );
  // Get props for the menu element
  let ref = ReactuseRef();
  let {
    menuProps
  } = useMenu(
    props
    state
    ref
  );
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        padding: 0
        listStyle:
          'none'
        border:
          '1px solid gray'
        maxWidth: 250
      }>
      [
        ...statecollection
      ]map((item) => (
        <MenuItem
          key=itemkey
          item=item
          state=state
          onAction=
            propsonAction
          
        />
      ))
    </ul>
  );
}
function MenuItem({
  item
  state
  onAction
}) {
  // Get props for the menu item element
  let ref = ReactuseRef();
  let {
    menuItemProps
  } = useMenuItem(
    {
      key: itemkey
      isDisabled:
        itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [
    isFocused
    setFocused
  ] = ReactuseState(
    false
  );
  let {
    focusProps
  } = useFocus({
    onFocusChange: setFocused
  });
  return (
    <li
      ...mergeProps(
        menuItemProps
        focusProps
      )
      ref=ref
      style={
        background: isFocused
          ? 'gray'
          : 'transparent'
        color: isFocused
          ? 'white'
          : null
        padding:
          '2px 5px'
        outline: 'none'
        cursor: 'pointer'
      }>
      itemrendered
    </li>
  );
}
<Menu
  onAction=alert
  aria-label="Actions">
  <Item key="one">
    One
  </Item>
  <Item key="two">
    Two
  </Item>
  <Item key="three">
    Three
  </Item>
</Menu>Sections#
This example shows how a menu can support sections with separators and headings
using props from useMenuSection.
This is accomplished using four extra elements: an <li> between the sections to
represent the separator, an <li> to contain the heading <span> element, and a
<ul> to contain the child items. This structure is necessary to ensure HTML
semantics are correct.
import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';
function Menu(props) {
  let state = useTreeState({...props selectionMode: 'none'});
  let ref = ReactuseRef();
  let {menuProps} = useMenu(props state ref);
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        margin: 0
        padding: 0
        listStyle: 'none'
        border: '1px solid gray'
        maxWidth: 250
      }>
      [...statecollection]map((item) => (
        <MenuSection
          key=itemkey
          section=item
          state=state
          onAction=propsonAction
        />
      ))
    </ul>
  );
}
function MenuSection({section state onAction}) {
  let {itemProps headingProps groupProps} = useMenuSection({
    heading: sectionrendered
    'aria-label': section['aria-label']
  });
  let {separatorProps} = useSeparator({
    elementType: 'li'
  });
  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      sectionkey !== statecollectiongetFirstKey() && (
        <li
          ...separatorProps
          style={
            borderTop: '1px solid gray'
            margin: '2px 5px'
          }
        />
      )
      <li ...itemProps>
        sectionrendered && (
          <span
            ...headingProps
            style={
              fontWeight: 'bold'
              fontSize: '1.1em'
              padding: '2px 5px'
            }>
            sectionrendered
          </span>
        )
        <ul
          ...groupProps
          style={
            padding: 0
            listStyle: 'none'
          }>
          [...sectionchildNodes]map((node) => (
            <MenuItem
              key=nodekey
              item=node
              state=state
              onAction=onAction
            />
          ))
        </ul>
      </li>
    </>
  );
}
function MenuItem({item state onAction}) {
  // Same as in the first example...
}
<Menu onAction=alert aria-label="Actions">
  <Section title="Section 1">
    <Item key="section1-item1">One</Item>
    <Item key="section1-item2">Two</Item>
    <Item key="section1-item3">Three</Item>
  </Section>
  <Section title="Section 2">
    <Item key="section2-item1">One</Item>
    <Item key="section2-item2">Two</Item>
    <Item key="section2-item3">Three</Item>
  </Section>
</Menu>import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';
function Menu(props) {
  let state = useTreeState({
    ...props
    selectionMode: 'none'
  });
  let ref = ReactuseRef();
  let {menuProps} = useMenu(props state ref);
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        margin: 0
        padding: 0
        listStyle: 'none'
        border: '1px solid gray'
        maxWidth: 250
      }>
      [...statecollection]map((item) => (
        <MenuSection
          key=itemkey
          section=item
          state=state
          onAction=propsonAction
        />
      ))
    </ul>
  );
}
function MenuSection({section state onAction}) {
  let {
    itemProps
    headingProps
    groupProps
  } = useMenuSection({
    heading: sectionrendered
    'aria-label': section['aria-label']
  });
  let {separatorProps} = useSeparator({
    elementType: 'li'
  });
  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      sectionkey !== statecollectiongetFirstKey() && (
        <li
          ...separatorProps
          style={
            borderTop: '1px solid gray'
            margin: '2px 5px'
          }
        />
      )
      <li ...itemProps>
        sectionrendered && (
          <span
            ...headingProps
            style={
              fontWeight: 'bold'
              fontSize: '1.1em'
              padding: '2px 5px'
            }>
            sectionrendered
          </span>
        )
        <ul
          ...groupProps
          style={
            padding: 0
            listStyle: 'none'
          }>
          [...sectionchildNodes]map((node) => (
            <MenuItem
              key=nodekey
              item=node
              state=state
              onAction=onAction
            />
          ))
        </ul>
      </li>
    </>
  );
}
function MenuItem({item state onAction}) {
  // Same as in the first example...
}
<Menu onAction=alert aria-label="Actions">
  <Section title="Section 1">
    <Item key="section1-item1">One</Item>
    <Item key="section1-item2">Two</Item>
    <Item key="section1-item3">Three</Item>
  </Section>
  <Section title="Section 2">
    <Item key="section2-item1">One</Item>
    <Item key="section2-item2">Two</Item>
    <Item key="section2-item3">Three</Item>
  </Section>
</Menu>import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';
function Menu(props) {
  let state = useTreeState(
    {
      ...props
      selectionMode:
        'none'
    }
  );
  let ref = ReactuseRef();
  let {
    menuProps
  } = useMenu(
    props
    state
    ref
  );
  return (
    <ul
      ...menuProps
      ref=ref
      style={
        margin: 0
        padding: 0
        listStyle:
          'none'
        border:
          '1px solid gray'
        maxWidth: 250
      }>
      [
        ...statecollection
      ]map((item) => (
        <MenuSection
          key=itemkey
          section=item
          state=state
          onAction=
            propsonAction
          
        />
      ))
    </ul>
  );
}
function MenuSection({
  section
  state
  onAction
}) {
  let {
    itemProps
    headingProps
    groupProps
  } = useMenuSection({
    heading:
      sectionrendered
    'aria-label':
      section[
        'aria-label'
      ]
  });
  let {
    separatorProps
  } = useSeparator({
    elementType: 'li'
  });
  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      sectionkey !==
        statecollectiongetFirstKey() && (
        <li
          ...separatorProps
          style={
            borderTop:
              '1px solid gray'
            margin:
              '2px 5px'
          }
        />
      )
      <li ...itemProps>
        sectionrendered && (
          <span
            ...headingProps
            style={
              fontWeight:
                'bold'
              fontSize:
                '1.1em'
              padding:
                '2px 5px'
            }>
            
              sectionrendered
            
          </span>
        )
        <ul
          ...groupProps
          style={
            padding: 0
            listStyle:
              'none'
          }>
          [
            ...sectionchildNodes
          ]map(
            (node) => (
              <MenuItem
                key=
                  nodekey
                
                item=
                  node
                
                state=
                  state
                
                onAction=
                  onAction
                
              />
            )
          )
        </ul>
      </li>
    </>
  );
}
function MenuItem({
  item
  state
  onAction
}) {
  // Same as in the first example...
}
<Menu
  onAction=alert
  aria-label="Actions">
  <Section title="Section 1">
    <Item key="section1-item1">
      One
    </Item>
    <Item key="section1-item2">
      Two
    </Item>
    <Item key="section1-item3">
      Three
    </Item>
  </Section>
  <Section title="Section 2">
    <Item key="section2-item1">
      One
    </Item>
    <Item key="section2-item2">
      Two
    </Item>
    <Item key="section2-item3">
      Three
    </Item>
  </Section>
</Menu>Complex menu items#
By default, menu items that only contain text will be labeled by the contents of the item.
For items that have more complex content (e.g. icons, multiple lines of text, keyboard shortcuts, etc.),
use labelProps, descriptionProps, and keyboardShortcutProps
from useMenuItem
as needed to apply to the main text element of the menu item, its description, and keyboard
shortcut text. This improves screen reader announcement.
NOTE: menu items cannot contain interactive content (e.g. buttons, checkboxes, etc.).
This example shows how labelProps, descriptionProps, and keyboardShortcutProps can be applied to
child elements of the item to apply ARIA properties returned
by useMenuItem. This is done using
React.cloneElement in this example, but you can use context or other approaches for this as well.
function Menu(props) {
  // Same as the first example...
}
function MenuItem({item state onAction}) {
  // Get props for the menu item element and child elements
  let ref = ReactuseRef();
  let {
    menuItemProps
    labelProps
    descriptionProps
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: itemkey
      isDisabled: itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused setFocused] = ReactuseState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});
  // Pull out the three expected children. We will clone them
  // and add the necessary props for accessibility.
  let [title description shortcut] = itemrendered;
  return (
    <li
      ...mergeProps(menuItemProps focusProps)
      ref=ref
      style={
        background: isFocused ? 'gray' : 'transparent'
        color: isFocused ? 'white' : null
        padding: '2px 5px'
        outline: 'none'
        cursor: 'pointer'
        display: 'flex'
        alignItems: 'center'
        justifyContent: 'space-between'
      }>
      <div>
        ReactcloneElement(title labelProps)
        ReactcloneElement(description descriptionProps)
      </div>
      ReactcloneElement(shortcut keyboardShortcutProps)
    </li>
  );
}
<Menu onAction=alert aria-label="Actions">
  <Item textValue="Copy" key="copy">
    <div>
      <strong>Copy</strong>
    </div>
    <div>Copy the selected text</div>
    <kbd>⌘C</kbd>
  </Item>
  <Item textValue="Cut" key="cut">
    <div>
      <strong>Cut</strong>
    </div>
    <div>Cut the selected text</div>
    <kbd>⌘X</kbd>
  </Item>
  <Item textValue="Paste" key="paste">
    <div>
      <strong>Paste</strong>
    </div>
    <div>Paste the copied text</div>
    <kbd>⌘V</kbd>
  </Item>
</Menu>function Menu(props) {
  // Same as the first example...
}
function MenuItem({item state onAction}) {
  // Get props for the menu item element and child elements
  let ref = ReactuseRef();
  let {
    menuItemProps
    labelProps
    descriptionProps
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: itemkey
      isDisabled: itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused setFocused] = ReactuseState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});
  // Pull out the three expected children. We will clone them
  // and add the necessary props for accessibility.
  let [title description shortcut] = itemrendered;
  return (
    <li
      ...mergeProps(menuItemProps focusProps)
      ref=ref
      style={
        background: isFocused ? 'gray' : 'transparent'
        color: isFocused ? 'white' : null
        padding: '2px 5px'
        outline: 'none'
        cursor: 'pointer'
        display: 'flex'
        alignItems: 'center'
        justifyContent: 'space-between'
      }>
      <div>
        ReactcloneElement(title labelProps)
        ReactcloneElement(description descriptionProps)
      </div>
      ReactcloneElement(shortcut keyboardShortcutProps)
    </li>
  );
}
<Menu onAction=alert aria-label="Actions">
  <Item textValue="Copy" key="copy">
    <div>
      <strong>Copy</strong>
    </div>
    <div>Copy the selected text</div>
    <kbd>⌘C</kbd>
  </Item>
  <Item textValue="Cut" key="cut">
    <div>
      <strong>Cut</strong>
    </div>
    <div>Cut the selected text</div>
    <kbd>⌘X</kbd>
  </Item>
  <Item textValue="Paste" key="paste">
    <div>
      <strong>Paste</strong>
    </div>
    <div>Paste the copied text</div>
    <kbd>⌘V</kbd>
  </Item>
</Menu>function Menu(props) {
  // Same as the first example...
}
function MenuItem({
  item
  state
  onAction
}) {
  // Get props for the menu item element and child elements
  let ref = ReactuseRef();
  let {
    menuItemProps
    labelProps
    descriptionProps
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: itemkey
      isDisabled:
        itemisDisabled
      onAction
    }
    state
    ref
  );
  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [
    isFocused
    setFocused
  ] = ReactuseState(
    false
  );
  let {
    focusProps
  } = useFocus({
    onFocusChange: setFocused
  });
  // Pull out the three expected children. We will clone them
  // and add the necessary props for accessibility.
  let [
    title
    description
    shortcut
  ] = itemrendered;
  return (
    <li
      ...mergeProps(
        menuItemProps
        focusProps
      )
      ref=ref
      style={
        background: isFocused
          ? 'gray'
          : 'transparent'
        color: isFocused
          ? 'white'
          : null
        padding:
          '2px 5px'
        outline: 'none'
        cursor:
          'pointer'
        display: 'flex'
        alignItems:
          'center'
        justifyContent:
          'space-between'
      }>
      <div>
        ReactcloneElement(
          title
          labelProps
        )
        ReactcloneElement(
          description
          descriptionProps
        )
      </div>
      ReactcloneElement(
        shortcut
        keyboardShortcutProps
      )
    </li>
  );
}
<Menu
  onAction=alert
  aria-label="Actions">
  <Item
    textValue="Copy"
    key="copy">
    <div>
      <strong>
        Copy
      </strong>
    </div>
    <div>
      Copy the selected
      text
    </div>
    <kbd>⌘C</kbd>
  </Item>
  <Item
    textValue="Cut"
    key="cut">
    <div>
      <strong>
        Cut
      </strong>
    </div>
    <div>
      Cut the selected
      text
    </div>
    <kbd>⌘X</kbd>
  </Item>
  <Item
    textValue="Paste"
    key="paste">
    <div>
      <strong>
        Paste
      </strong>
    </div>
    <div>
      Paste the copied
      text
    </div>
    <kbd>⌘V</kbd>
  </Item>
</Menu>Internationalization#
useMenu handles some aspects of internationalization automatically.
For example, type to select is implemented with an
Intl.Collator
for internationalized string matching. You are responsible for localizing all menu item labels for
content that is passed into the menu.
RTL#
In right-to-left languages, the menu items should be mirrored. The text content should be aligned to the right, and keyboard shortcuts should be aligned left. Ensure that your CSS accounts for this.