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.

installyarn add @react-aria/menu
version3.2.3
usageimport {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 )): MenuSectionAria

Features#


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 menu with menuitem children 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#


Menu anatomy diagramMenu itemMenu item keyboard shortcutMenu item labelMenu item descriptionMenuSection headingMenu item descriptionOption 1Option 2DescriptionDescriptionOption 3DescriptionSECTION TITLEGroupK

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:

NameTypeDescription
menuPropsHTMLAttributes<HTMLElement>Props for the menu element.

useMenuItem returns props for an individual item and its children:

NameTypeDescription
menuItemPropsHTMLAttributes<HTMLElement>Props for the menu item element.
labelPropsHTMLAttributes<HTMLElement>Props for the main text element inside the menu item.
descriptionPropsHTMLAttributes<HTMLElement>Props for the description text element inside the menu item, if any.
keyboardShortcutPropsHTMLAttributes<HTMLElement>Props for the keyboard shortcut text element inside the item, if any.

useMenuSection returns props for a section:

NameTypeDescription
itemPropsHTMLAttributes<HTMLElement>Props for the wrapper list item.
headingPropsHTMLAttributes<HTMLElement>Props for the heading element, if any.
groupPropsHTMLAttributes<HTMLElement>Props for the group element.

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 with the first item disabled, 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 = React.useRef();
  let {menuProps} = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        padding: 0,
        listStyle: 'none',
        border: '1px solid gray',
        maxWidth: 250
      }}>
      {[...state.collection].map((item) => (
        <MenuItem
          key={item.key}
          item={item}
          state={state}
          onAction={props.onAction}
        />
      ))}
    </ul>
  );
}

function MenuItem({item, state, onAction}) {
  // Get props for the menu item element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);

  let {menuItemProps} = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused, setFocused] = React.useState(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: isDisabled ? 'default' : 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Menu onAction={alert} aria-label="Actions" disabledKeys={['one']}>
  <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 = React.useRef();
  let {menuProps} = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        padding: 0,
        listStyle: 'none',
        border: '1px solid gray',
        maxWidth: 250
      }}>
      {[...state.collection].map((item) => (
        <MenuItem
          key={item.key}
          item={item}
          state={state}
          onAction={props.onAction}
        />
      ))}
    </ul>
  );
}

function MenuItem({item, state, onAction}) {
  // Get props for the menu item element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);

  let {menuItemProps} = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused, setFocused] = React.useState(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: isDisabled ? 'default' : 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Menu
  onAction={alert}
  aria-label="Actions"
  disabledKeys={['one']}>
  <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 = React.useRef();
  let {
    menuProps
  } = useMenu(
    props,
    state,
    ref
  );

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        padding: 0,
        listStyle:
          'none',
        border:
          '1px solid gray',
        maxWidth: 250
      }}>
      {[
        ...state.collection
      ].map((item) => (
        <MenuItem
          key={item.key}
          item={item}
          state={state}
          onAction={
            props.onAction
          }
        />
      ))}
    </ul>
  );
}

function MenuItem({
  item,
  state,
  onAction
}) {
  // Get props for the menu item element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(
    item.key
  );

  let {
    menuItemProps
  } = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [
    isFocused,
    setFocused
  ] = React.useState(
    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: isDisabled
          ? 'default'
          : 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Menu
  onAction={alert}
  aria-label="Actions"
  disabledKeys={[
    'one'
  ]}>
  <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 = React.useRef();
  let {menuProps} = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        border: '1px solid gray',
        maxWidth: 250
      }}>
      {[...state.collection].map((item) => (
        <MenuSection
          key={item.key}
          section={item}
          state={state}
          onAction={props.onAction}
        />
      ))}
    </ul>
  );
}

function MenuSection({section, state, onAction}) {
  let {itemProps, headingProps, groupProps} = useMenuSection({
    heading: section.rendered,
    '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 (
    <>
      {section.key !== state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop: '1px solid gray',
            margin: '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight: 'bold',
              fontSize: '1.1em',
              padding: '2px 5px'
            }}>
            {section.rendered}
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle: 'none'
          }}>
          {[...section.childNodes].map((node) => (
            <MenuItem
              key={node.key}
              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 = React.useRef();
  let {menuProps} = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        border: '1px solid gray',
        maxWidth: 250
      }}>
      {[...state.collection].map((item) => (
        <MenuSection
          key={item.key}
          section={item}
          state={state}
          onAction={props.onAction}
        />
      ))}
    </ul>
  );
}

function MenuSection({section, state, onAction}) {
  let {
    itemProps,
    headingProps,
    groupProps
  } = useMenuSection({
    heading: section.rendered,
    '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 (
    <>
      {section.key !== state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop: '1px solid gray',
            margin: '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight: 'bold',
              fontSize: '1.1em',
              padding: '2px 5px'
            }}>
            {section.rendered}
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle: 'none'
          }}>
          {[...section.childNodes].map((node) => (
            <MenuItem
              key={node.key}
              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 = React.useRef();
  let {
    menuProps
  } = useMenu(
    props,
    state,
    ref
  );

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle:
          'none',
        border:
          '1px solid gray',
        maxWidth: 250
      }}>
      {[
        ...state.collection
      ].map((item) => (
        <MenuSection
          key={item.key}
          section={item}
          state={state}
          onAction={
            props.onAction
          }
        />
      ))}
    </ul>
  );
}

function MenuSection({
  section,
  state,
  onAction
}) {
  let {
    itemProps,
    headingProps,
    groupProps
  } = useMenuSection({
    heading:
      section.rendered,
    '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 (
    <>
      {section.key !==
        state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop:
              '1px solid gray',
            margin:
              '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight:
                'bold',
              fontSize:
                '1.1em',
              padding:
                '2px 5px'
            }}>
            {
              section.rendered
            }
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle:
              'none'
          }}>
          {[
            ...section.childNodes
          ].map(
            (node) => (
              <MenuItem
                key={
                  node.key
                }
                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 = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);

  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused, setFocused] = React.useState(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] = item.rendered;

  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>
        {React.cloneElement(title, labelProps)}
        {React.cloneElement(description, descriptionProps)}
      </div>
      {React.cloneElement(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 = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);

  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [isFocused, setFocused] = React.useState(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] = item.rendered;

  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>
        {React.cloneElement(title, labelProps)}
        {React.cloneElement(description, descriptionProps)}
      </div>
      {React.cloneElement(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 = React.useRef();
  let isDisabled = state.disabledKeys.has(
    item.key
  );

  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps
  } = useMenuItem(
    {
      key: item.key,
      isDisabled,
      onAction
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused menu item
  let [
    isFocused,
    setFocused
  ] = React.useState(
    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
  ] = item.rendered;

  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>
        {React.cloneElement(
          title,
          labelProps
        )}
        {React.cloneElement(
          description,
          descriptionProps
        )}
      </div>
      {React.cloneElement(
        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.