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
version3.19.0
usageimport {useMenuTrigger, useMenu, useMenuItem, useMenuSection} from 'react-aria'

API#


useMenuTrigger<T>( props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject<Element> ): MenuTriggerAria<T> useMenu<T>( props: AriaMenuOptions<T>, state: TreeState<T>, ref: RefObject<HTMLElement> ): MenuAria useMenuItem<T>( props: AriaMenuItemProps, state: TreeState<T>, ref: RefObject<FocusableElement> ): MenuItemAria useMenuSection( (props: AriaMenuSectionProps )): MenuSectionAria

Features#


There is no native element to implement a menu in HTML that is widely supported. useMenuTrigger and useMenu help achieve accessible menu components that can be styled as needed.

  • Exposed to assistive technology as a button with a menu using ARIA
  • Support for single, multiple, or no selection
  • Support for disabled items
  • Support for sections
  • Complex item labeling support for accessibility
  • Keyboard navigation support including arrow keys, home/end, page up/down
  • Automatic scrolling support during keyboard navigation
  • Keyboard support for opening the menu using the arrow keys, including automatically focusing the first or last item accordingly
  • Typeahead to allow focusing items by typing text
  • Virtualized scrolling support for performance with long lists

Anatomy#


Option 1Option 2Menu itemMenu item keyboard shortcutMenu item labelDescriptionDescriptionOption 3DescriptionMenu item descriptionMenuSECTION TITLESection headingGroupKMenu item descriptionMore ActionsMenu Trigger

A menu trigger consists of a button or other trigger element combined with a popup menu, with a list of menu items or groups inside. useMenuTrigger, 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.

useMenuTrigger returns props that you should spread onto the appropriate element:

NameTypeDescription
menuTriggerPropsAriaButtonPropsProps for the menu trigger element.
menuPropsAriaMenuOptions<T>Props for the menu.

useMenu returns props that you should spread onto the menu container element:

NameTypeDescription
menuPropsDOMAttributesProps for the menu element.

useMenuItem returns props for an individual item and its children:

NameTypeDescription
menuItemPropsDOMAttributesProps for the menu item element.
labelPropsDOMAttributesProps for the main text element inside the menu item.
descriptionPropsDOMAttributesProps for the description text element inside the menu item, if any.
keyboardShortcutPropsDOMAttributesProps for the keyboard shortcut text element inside the item, if any.
isFocusedbooleanWhether the item is currently focused.
isSelectedbooleanWhether the item is currently selected.
isPressedbooleanWhether the item is currently in a pressed state.
isDisabledbooleanWhether the item is disabled.

useMenuSection returns props for a section:

NameTypeDescription
itemPropsDOMAttributesProps for the wrapper list item.
headingPropsDOMAttributesProps for the heading element, if any.
groupPropsDOMAttributesProps for the group element.

State for the trigger is managed by the useMenuTriggerState hook from @react-stately/menu. State for the menu itself is managed by the useTreeState hook from @react-stately/tree. These state objects should be passed to the appropriate React Aria 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.

Example#


A menu consists of several components: a menu button to toggle the menu popup, and the menu itself, which contains items or sections of items. We'll go through each component one by one.

We'll start with the MenuButton component, which is what will trigger our menu to appear. This uses the useMenuTrigger and useMenuTriggerState hooks. The Popover and Button components used in this example are independent, and can be shared by many other components. The code is available below, and documentation is available on the corresponding pages.

import {useMenuTrigger} from 'react-aria';
import {Item, useMenuTriggerState} from 'react-stately';

// Reuse the Popover, and Button from your component library. See below for details.
import {Button, Popover} from 'your-component-library';

function MenuButton(props) {
  // Create state based on the incoming props
  let state = useMenuTriggerState(props);

  // Get props for the button and menu elements
  let ref = React.useRef();
  let { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref);

  return (
    <div style={{ position: 'relative', display: 'inline-block' }}>
      <Button
        {...menuTriggerProps}
        buttonRef={ref}
        style={{ height: 30, fontSize: 14 }}
      >
        {props.label}
        <span aria-hidden="true" style={{ paddingLeft: 5 }}></span>
      </Button>
      {state.isOpen &&
        (
          <Popover isOpen={state.isOpen} onClose={state.close}>
            <Menu
              {...props}
              {...menuProps}
            />
          </Popover>
        )}
    </div>
  );
}
import {useMenuTrigger} from 'react-aria';
import {Item, useMenuTriggerState} from 'react-stately';

// Reuse the Popover, and Button from your component library. See below for details.
import {Button, Popover} from 'your-component-library';

function MenuButton(props) {
  // Create state based on the incoming props
  let state = useMenuTriggerState(props);

  // Get props for the button and menu elements
  let ref = React.useRef();
  let { menuTriggerProps, menuProps } = useMenuTrigger(
    {},
    state,
    ref
  );

  return (
    <div
      style={{
        position: 'relative',
        display: 'inline-block'
      }}
    >
      <Button
        {...menuTriggerProps}
        buttonRef={ref}
        style={{ height: 30, fontSize: 14 }}
      >
        {props.label}
        <span aria-hidden="true" style={{ paddingLeft: 5 }}></span>
      </Button>
      {state.isOpen &&
        (
          <Popover
            isOpen={state.isOpen}
            onClose={state.close}
          >
            <Menu
              {...props}
              {...menuProps}
            />
          </Popover>
        )}
    </div>
  );
}
import {useMenuTrigger} from 'react-aria';
import {
  Item,
  useMenuTriggerState
} from 'react-stately';

// Reuse the Popover, and Button from your component library. See below for details.
import {
  Button,
  Popover
} from 'your-component-library';

function MenuButton(
  props
) {
  // Create state based on the incoming props
  let state =
    useMenuTriggerState(
      props
    );

  // Get props for the button and menu elements
  let ref = React
    .useRef();
  let {
    menuTriggerProps,
    menuProps
  } = useMenuTrigger(
    {},
    state,
    ref
  );

  return (
    <div
      style={{
        position:
          'relative',
        display:
          'inline-block'
      }}
    >
      <Button
        {...menuTriggerProps}
        buttonRef={ref}
        style={{
          height: 30,
          fontSize: 14
        }}
      >
        {props.label}
        <span
          aria-hidden="true"
          style={{
            paddingLeft:
              5
          }}
        ></span>
      </Button>
      {state.isOpen &&
        (
          <Popover
            isOpen={state
              .isOpen}
            onClose={state
              .close}
          >
            <Menu
              {...props}
              {...menuProps}
            />
          </Popover>
        )}
    </div>
  );
}

Next, let's implement the Menu component. This will appear inside the Popover when the user presses the button. It is built using the useMenu and useTreeState hooks. For each item in the collection in state, we render either a MenuItem or MenuSection (defined below) according to the item's type property.

import {useTreeState} from 'react-stately';
import {useMenu} from 'react-aria';

function Menu(props) {
  // Create menu state based on the incoming props
  let state = useTreeState(props);

  // Get props for the menu element
  let ref = React.useRef();
  let { menuProps } = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        width: 150
      }}
    >
      {[...state.collection].map((item) => (
        item.type === 'section'
          ? <MenuSection key={item.key} section={item} state={state} />
          : <MenuItem key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}
import {useTreeState} from 'react-stately';
import {useMenu} from 'react-aria';

function Menu(props) {
  // Create menu state based on the incoming props
  let state = useTreeState(props);

  // Get props for the menu element
  let ref = React.useRef();
  let { menuProps } = useMenu(props, state, ref);

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        width: 150
      }}
    >
      {[...state.collection].map((item) => (
        item.type === 'section'
          ? (
            <MenuSection
              key={item.key}
              section={item}
              state={state}
            />
          )
          : (
            <MenuItem
              key={item.key}
              item={item}
              state={state}
            />
          )
      ))}
    </ul>
  );
}
import {useTreeState} from 'react-stately';
import {useMenu} from 'react-aria';

function Menu(props) {
  // Create menu state based on the incoming props
  let state =
    useTreeState(props);

  // Get props for the menu element
  let ref = React
    .useRef();
  let { menuProps } =
    useMenu(
      props,
      state,
      ref
    );

  return (
    <ul
      {...menuProps}
      ref={ref}
      style={{
        margin: 0,
        padding: 0,
        listStyle:
          'none',
        width: 150
      }}
    >
      {[
        ...state
          .collection
      ].map((item) => (
        item.type ===
            'section'
          ? (
            <MenuSection
              key={item
                .key}
              section={item}
              state={state}
            />
          )
          : (
            <MenuItem
              key={item
                .key}
              item={item}
              state={state}
            />
          )
      ))}
    </ul>
  );
}

Now let's implement MenuItem. This is built using useMenuItem, and the state object passed via props from Menu.

import {useMenuItem} from 'react-aria';

function MenuItem({ item, state }) {
  // Get props for the menu item element
  let ref = React.useRef();
  let { menuItemProps, isFocused, isSelected, isDisabled } = useMenuItem(
    { key: item.key },
    state,
    ref
  );

  return (
    <li
      {...menuItemProps}
      ref={ref}
      style={{
        background: isFocused ? 'gray' : 'transparent',
        color: isDisabled ? 'gray' : isFocused ? 'white' : 'black',
        padding: '2px 5px',
        outline: 'none',
        cursor: 'default',
        display: 'flex',
        justifyContent: 'space-between'
      }}
    >
      {item.rendered}
      {isSelected && <span aria-hidden="true"></span>}
    </li>
  );
}
import {useMenuItem} from 'react-aria';

function MenuItem({ item, state }) {
  // Get props for the menu item element
  let ref = React.useRef();
  let { menuItemProps, isFocused, isSelected, isDisabled } =
    useMenuItem({ key: item.key }, state, ref);

  return (
    <li
      {...menuItemProps}
      ref={ref}
      style={{
        background: isFocused ? 'gray' : 'transparent',
        color: isDisabled
          ? 'gray'
          : isFocused
          ? 'white'
          : 'black',
        padding: '2px 5px',
        outline: 'none',
        cursor: 'default',
        display: 'flex',
        justifyContent: 'space-between'
      }}
    >
      {item.rendered}
      {isSelected && <span aria-hidden="true"></span>}
    </li>
  );
}
import {useMenuItem} from 'react-aria';

function MenuItem(
  { item, state }
) {
  // Get props for the menu item element
  let ref = React
    .useRef();
  let {
    menuItemProps,
    isFocused,
    isSelected,
    isDisabled
  } = useMenuItem(
    { key: item.key },
    state,
    ref
  );

  return (
    <li
      {...menuItemProps}
      ref={ref}
      style={{
        background:
          isFocused
            ? 'gray'
            : 'transparent',
        color: isDisabled
          ? 'gray'
          : isFocused
          ? 'white'
          : 'black',
        padding:
          '2px 5px',
        outline: 'none',
        cursor:
          'default',
        display: 'flex',
        justifyContent:
          'space-between'
      }}
    >
      {item.rendered}
      {isSelected && (
        <span aria-hidden="true"></span>
      )}
    </li>
  );
}

Now we can render a simple menu with actionable items:

<MenuButton label="Actions" onAction={alert}>
  <Item key="copy">Copy</Item>
  <Item key="cut">Cut</Item>
  <Item key="paste">Paste</Item>
</MenuButton>
<MenuButton label="Actions" onAction={alert}>
  <Item key="copy">Copy</Item>
  <Item key="cut">Cut</Item>
  <Item key="paste">Paste</Item>
</MenuButton>
<MenuButton
  label="Actions"
  onAction={alert}
>
  <Item key="copy">
    Copy
  </Item>
  <Item key="cut">
    Cut
  </Item>
  <Item key="paste">
    Paste
  </Item>
</MenuButton>

Popover#

The Popover component is used to contain the menu. It can be shared between many other components, including ComboBox, Select, Dialog, and others. See useOverlayTrigger for more examples of popovers.

Show code
import {DismissButton, FocusScope, useOverlay} from 'react-aria';

function Popover(props) {
  let ref = React.useRef();
  let {
    popoverRef = ref,
    isOpen,
    onClose,
    children
  } = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let { overlayProps } = useOverlay({
    isOpen,
    onClose,
    shouldCloseOnBlur: true,
    isDismissable: true
  }, popoverRef);

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position: 'absolute',
          minWidth: '100%',
          border: '1px solid gray',
          background: 'lightgray',
          marginTop: 4
        }}
      >
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
}
import {
  DismissButton,
  FocusScope,
  useOverlay
} from 'react-aria';

function Popover(props) {
  let ref = React.useRef();
  let {
    popoverRef = ref,
    isOpen,
    onClose,
    children
  } = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let { overlayProps } = useOverlay({
    isOpen,
    onClose,
    shouldCloseOnBlur: true,
    isDismissable: true
  }, popoverRef);

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position: 'absolute',
          minWidth: '100%',
          border: '1px solid gray',
          background: 'lightgray',
          marginTop: 4
        }}
      >
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
}
import {
  DismissButton,
  FocusScope,
  useOverlay
} from 'react-aria';

function Popover(props) {
  let ref = React
    .useRef();
  let {
    popoverRef = ref,
    isOpen,
    onClose,
    children
  } = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let { overlayProps } =
    useOverlay({
      isOpen,
      onClose,
      shouldCloseOnBlur:
        true,
      isDismissable: true
    }, popoverRef);

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope
      restoreFocus
    >
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position:
            'absolute',
          minWidth:
            '100%',
          border:
            '1px solid gray',
          background:
            'lightgray',
          marginTop: 4
        }}
      >
        {children}
        <DismissButton
          onDismiss={onClose}
        />
      </div>
    </FocusScope>
  );
}

Button#

The Button component is used in the above example to toggle the menu. It is built using the useButton hook, and can be shared with many other components.

Show code
import {useButton} from 'react-aria';

function Button(props) {
  let ref = props.buttonRef;
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref} style={props.style}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = props.buttonRef;
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref} style={props.style}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref =
    props.buttonRef;
  let { buttonProps } =
    useButton(
      props,
      ref
    );
  return (
    <button
      {...buttonProps}
      ref={ref}
      style={props.style}
    >
      {props.children}
    </button>
  );
}

Styled examples#


Tailwind CSS
An example of styling a Menu with Tailwind.

Dynamic collections#


Menu follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.

As seen below, an iterable list of options is passed to the ComboBox using the items prop. Each item accepts a key prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and a key prop is not required.

function Example() {
  let items = [
    {id: 1, name: 'New'},
    {id: 2, name: 'Open'},
    {id: 3, name: 'Close'},
    {id: 4, name: 'Save'},
    {id: 5, name: 'Duplicate'},
    {id: 6, name: 'Rename'},
    {id: 7, name: 'Move'}
  ];

  return (
    <MenuButton label="Actions" items={items} onAction={alert}>
      {(item) => <Item>{item.name}</Item>}
    </MenuButton>
  );
}
function Example() {
  let items = [
    { id: 1, name: 'New' },
    { id: 2, name: 'Open' },
    { id: 3, name: 'Close' },
    { id: 4, name: 'Save' },
    { id: 5, name: 'Duplicate' },
    { id: 6, name: 'Rename' },
    { id: 7, name: 'Move' }
  ];

  return (
    <MenuButton
      label="Actions"
      items={items}
      onAction={alert}
    >
      {(item) => <Item>{item.name}</Item>}
    </MenuButton>
  );
}
function Example() {
  let items = [
    {
      id: 1,
      name: 'New'
    },
    {
      id: 2,
      name: 'Open'
    },
    {
      id: 3,
      name: 'Close'
    },
    {
      id: 4,
      name: 'Save'
    },
    {
      id: 5,
      name: 'Duplicate'
    },
    {
      id: 6,
      name: 'Rename'
    },
    {
      id: 7,
      name: 'Move'
    }
  ];

  return (
    <MenuButton
      label="Actions"
      items={items}
      onAction={alert}
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </MenuButton>
  );
}

Selection#


Menu supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected items (uncontrolled) and selectedKeys to set the selected items (controlled). The value of the selected keys must match the key prop of the items. See the react-stately Selection docs for more details.

function Example() {
  let [selected, setSelected] = React.useState(new Set(['sidebar', 'console']));

  return (
    <>
      <MenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="sidebar">Sidebar</Item>
        <Item key="searchbar">Searchbar</Item>
        <Item key="tools">Tools</Item>
        <Item key="console">Console</Item>
      </MenuButton>
      <p>Current selection (controlled): {[...selected].join(', ')}</p>
    </>
  );
}
function Example() {
  let [selected, setSelected] = React.useState(
    new Set(['sidebar', 'console'])
  );

  return (
    <>
      <MenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="sidebar">Sidebar</Item>
        <Item key="searchbar">Searchbar</Item>
        <Item key="tools">Tools</Item>
        <Item key="console">Console</Item>
      </MenuButton>
      <p>
        Current selection (controlled):{' '}
        {[...selected].join(', ')}
      </p>
    </>
  );
}
function Example() {
  let [
    selected,
    setSelected
  ] = React.useState(
    new Set([
      'sidebar',
      'console'
    ])
  );

  return (
    <>
      <MenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="sidebar">
          Sidebar
        </Item>
        <Item key="searchbar">
          Searchbar
        </Item>
        <Item key="tools">
          Tools
        </Item>
        <Item key="console">
          Console
        </Item>
      </MenuButton>
      <p>
        Current selection
        (controlled):
        {' '}
        {[...selected]
          .join(', ')}
      </p>
    </>
  );
}

Sections#


Menu supports sections with separators and headings in order to group options. Sections can be used by wrapping groups of Items in a Section component. Each Section takes a title and key prop. To implement sections, implement the ListBoxSection component referenced above using the useMenuSection hook. It will include 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 {useMenuSection, useSeparator} from 'react-aria';

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>
    </>
  );
}
import {useMenuSection, useSeparator} from 'react-aria';

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>
    </>
  );
}
import {
  useMenuSection,
  useSeparator
} from 'react-aria';

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>
    </>
  );
}

Static items#

With this in place, we can now render a static menu with multiple sections:

import {Section} from 'react-stately';

<MenuButton label="Actions" onAction={alert}>
  <Section title="Styles">
    <Item key="bold">Bold</Item>
    <Item key="underline">Underline</Item>
  </Section>
  <Section title="Align">
    <Item key="left">Left</Item>
    <Item key="middle">Middle</Item>
    <Item key="right">Right</Item>
  </Section>
</MenuButton>
import {Section} from 'react-stately';

<MenuButton label="Actions" onAction={alert}>
  <Section title="Styles">
    <Item key="bold">Bold</Item>
    <Item key="underline">Underline</Item>
  </Section>
  <Section title="Align">
    <Item key="left">Left</Item>
    <Item key="middle">Middle</Item>
    <Item key="right">Right</Item>
  </Section>
</MenuButton>
import {Section} from 'react-stately';

<MenuButton
  label="Actions"
  onAction={alert}
>
  <Section title="Styles">
    <Item key="bold">
      Bold
    </Item>
    <Item key="underline">
      Underline
    </Item>
  </Section>
  <Section title="Align">
    <Item key="left">
      Left
    </Item>
    <Item key="middle">
      Middle
    </Item>
    <Item key="right">
      Right
    </Item>
  </Section>
</MenuButton>

Dynamic items#

The above example shows sections with static items. Sections can also be populated from a heirarchical data structure. Similarly to the props on Menu, <Section> takes an array of data using the items prop.

function Example() {
  let [selected, setSelected] = React.useState(new Set([1,3]));
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        {id: 1, name: 'Final Copy (1)'}
      ]
    },
    {
      name: 'Right Panel',
      id: 'right',
      children: [
        {id: 2, name: 'index.ts'},
        {id: 3, name: 'package.json'},
        {id: 4, name: 'license.txt'}
      ]
    }
  ];

  return (
    <MenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}>
      {item => (
        <Section items={item.children} title={item.name}>
          {item => <Item>{item.name}</Item>}
        </Section>
      )}
    </MenuButton>
  );
}
function Example() {
  let [selected, setSelected] = React.useState(
    new Set([1, 3])
  );
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        { id: 1, name: 'Final Copy (1)' }
      ]
    },
    {
      name: 'Right Panel',
      id: 'right',
      children: [
        { id: 2, name: 'index.ts' },
        { id: 3, name: 'package.json' },
        { id: 4, name: 'license.txt' }
      ]
    }
  ];

  return (
    <MenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}
    >
      {(item) => (
        <Section items={item.children} title={item.name}>
          {(item) => <Item>{item.name}</Item>}
        </Section>
      )}
    </MenuButton>
  );
}
function Example() {
  let [
    selected,
    setSelected
  ] = React.useState(
    new Set([1, 3])
  );
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        {
          id: 1,
          name:
            'Final Copy (1)'
        }
      ]
    },
    {
      name:
        'Right Panel',
      id: 'right',
      children: [
        {
          id: 2,
          name:
            'index.ts'
        },
        {
          id: 3,
          name:
            'package.json'
        },
        {
          id: 4,
          name:
            'license.txt'
        }
      ]
    }
  ];

  return (
    <MenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}
    >
      {(item) => (
        <Section
          items={item
            .children}
          title={item
            .name}
        >
          {(item) => (
            <Item>
              {item.name}
            </Item>
          )}
        </Section>
      )}
    </MenuButton>
  );
}

Accessibility#

Sections without a title must provide an aria-label for accessibility.

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.).

To implement this, we'll update the MenuItem component to apply the ARIA properties returned by useMenuItem to the appropriate elements. In this example, we'll pull them out of props.children and use React.cloneElement to apply the props, but you may want to use a more robust approach (e.g. context).

function MenuItem({item, state}) {
  // Get props for the menu item element and child elements
  let ref = React.useRef();
  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps,
    isFocused
  } = useMenuItem({key: item.key}, state, ref);

  // 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
      {...menuItemProps}
      ref={ref}
      style={{
        background: isFocused ? 'gray' : 'transparent',
        color: isFocused ? 'white' : null,
        padding: '2px 5px',
        outline: 'none',
        cursor: 'default',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between'
      }}>
      <div>
        {React.cloneElement(title, labelProps)}
        {React.cloneElement(description, descriptionProps)}
      </div>
      {React.cloneElement(shortcut, keyboardShortcutProps)}
    </li>
  );
}

<MenuButton label="Actions" onAction={alert}>
  <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>
</MenuButton>
function MenuItem({ item, state }) {
  // Get props for the menu item element and child elements
  let ref = React.useRef();
  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps,
    isFocused
  } = useMenuItem({ key: item.key }, state, ref);

  // 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
      {...menuItemProps}
      ref={ref}
      style={{
        background: isFocused ? 'gray' : 'transparent',
        color: isFocused ? 'white' : null,
        padding: '2px 5px',
        outline: 'none',
        cursor: 'default',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between'
      }}
    >
      <div>
        {React.cloneElement(title, labelProps)}
        {React.cloneElement(description, descriptionProps)}
      </div>
      {React.cloneElement(shortcut, keyboardShortcutProps)}
    </li>
  );
}

<MenuButton label="Actions" onAction={alert}>
  <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>
</MenuButton>
function MenuItem(
  { item, state }
) {
  // Get props for the menu item element and child elements
  let ref = React
    .useRef();
  let {
    menuItemProps,
    labelProps,
    descriptionProps,
    keyboardShortcutProps,
    isFocused
  } = useMenuItem(
    { key: item.key },
    state,
    ref
  );

  // 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
      {...menuItemProps}
      ref={ref}
      style={{
        background:
          isFocused
            ? 'gray'
            : 'transparent',
        color: isFocused
          ? 'white'
          : null,
        padding:
          '2px 5px',
        outline: 'none',
        cursor:
          'default',
        display: 'flex',
        alignItems:
          'center',
        justifyContent:
          'space-between'
      }}
    >
      <div>
        {React
          .cloneElement(
            title,
            labelProps
          )}
        {React
          .cloneElement(
            description,
            descriptionProps
          )}
      </div>
      {React
        .cloneElement(
          shortcut,
          keyboardShortcutProps
        )}
    </li>
  );
}

<MenuButton
  label="Actions"
  onAction={alert}
>
  <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>
</MenuButton>

Disabled items#


useMenu supports marking items as disabled using the disabledKeys prop. Each key in this list corresponds with the key prop passed to the Item component, or automatically derived from the values passed to the items prop. See Collections for more details.

Disabled items are not focusable or keyboard navigable, and do not trigger onAction or onSelectionChange. The isDisabled property returned by useMenuItem can be used to style the item appropriately.

<MenuButton label="Actions" onAction={alert} disabledKeys={['paste']}>
  <Item key="copy">Copy</Item>
  <Item key="cut">Cut</Item>
  <Item key="paste">Paste</Item>
</MenuButton>
<MenuButton
  label="Actions"
  onAction={alert}
  disabledKeys={['paste']}
>
  <Item key="copy">Copy</Item>
  <Item key="cut">Cut</Item>
  <Item key="paste">Paste</Item>
</MenuButton>
<MenuButton
  label="Actions"
  onAction={alert}
  disabledKeys={[
    'paste'
  ]}
>
  <Item key="copy">
    Copy
  </Item>
  <Item key="cut">
    Cut
  </Item>
  <Item key="paste">
    Paste
  </Item>
</MenuButton>

Controlled open state#


The open state of the menu can be controlled via the defaultOpen and isOpen props

function Example() {
  let [open, setOpen] = React.useState(false);

  return (
    <>
      <p>Menu is {open ? 'open' : 'closed'}</p>
      <MenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}>
        <Item key="side">Side bar</Item>
        <Item key="options">Page options</Item>
        <Item key="edit">Edit Panel</Item>
      </MenuButton>
    </>
  );
}
function Example() {
  let [open, setOpen] = React.useState(false);

  return (
    <>
      <p>Menu is {open ? 'open' : 'closed'}</p>
      <MenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}>
        <Item key="side">Side bar</Item>
        <Item key="options">Page options</Item>
        <Item key="edit">Edit Panel</Item>
      </MenuButton>
    </>
  );
}
function Example() {
  let [open, setOpen] =
    React.useState(
      false
    );

  return (
    <>
      <p>
        Menu is {open
          ? 'open'
          : 'closed'}
      </p>
      <MenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}
      >
        <Item key="side">
          Side bar
        </Item>
        <Item key="options">
          Page options
        </Item>
        <Item key="edit">
          Edit Panel
        </Item>
      </MenuButton>
    </>
  );
}

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 button should be mirrored. The arrow should be on the left, and the label should be on the right. In addition, the content of menu items should flip. Ensure that your CSS accounts for this.