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.4.3 |
usage | import {useMenu, useMenuItem, useMenuSection} from '@react-aria/menu' |
API#
useMenu<T>(
props: AriaMenuOptions<T>,
state: TreeState<T>,
ref: RefObject<HTMLElement>
): MenuAria
useMenuItem<T>(
props: AriaMenuItemProps,
state: TreeState<T>,
ref: RefObject<HTMLElement>
): MenuItemAria
useMenuSection(
(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
withmenuitem
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#
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 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 {useMenu, useMenuItem} from '@react-aria/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 isFocused = state.selectionManager.focusedKey === item.key;
let {menuItemProps} = useMenuItem({
key: item.key,
isDisabled,
onAction
}, state, ref);
return (
<li
{...menuItemProps}
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 {useMenu, useMenuItem} from '@react-aria/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 isFocused =
state.selectionManager.focusedKey === item.key;
let { menuItemProps } = useMenuItem(
{
key: item.key,
isDisabled,
onAction
},
state,
ref
);
return (
<li
{...menuItemProps}
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 {
useMenu,
useMenuItem
} from '@react-aria/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 isFocused =
state
.selectionManager
.focusedKey ===
item.key;
let { menuItemProps } =
useMenuItem(
{
key: item.key,
isDisabled,
onAction
},
state,
ref
);
return (
<li
{...menuItemProps}
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 {useMenuSection} from '@react-aria/menu';
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 {useMenuSection} from '@react-aria/menu';
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 {useMenuSection} from '@react-aria/menu';
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.