packages/react-aria/docs/menu/useMenu.mdx
{/* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}
import {Layout} from '@react-spectrum/docs'; export default Layout;
import docs from 'docs:@react-aria/menu'; import overlaysDocs from 'docs:@react-aria/overlays'; import focusDocs from 'docs:@react-aria/focus'; import menuDocs from 'docs:@react-aria/menu'; import buttonDocs from 'docs:@react-aria/button'; import statelyDocs from 'docs:@react-stately/menu'; import treeDocs from 'docs:@react-stately/tree'; import utilsDocs from 'docs:@react-aria/utils'; import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescription} from '@react-spectrum/docs'; import packageData from '@react-aria/menu/package.json'; import Anatomy from './menu-trigger-anatomy.svg'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard'; import tailwindPreview from 'url:./tailwind.png';
<PageDescription>{docs.exports.useMenu.description}</PageDescription>
<HeaderInfo packageData={packageData} componentNames={['useMenuTrigger', 'useMenu', 'useMenuItem', 'useMenuSection']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu/'} ]} />
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.
menu using ARIAA 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:
<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useMenuTrigger.return.base.id].properties} /> </TypeContext.Provider>
useMenu returns props that you should spread onto the menu container element:
<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useMenu.return.id].properties} /> </TypeContext.Provider>
useMenuItem returns props for an individual item and its children:
<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useMenuItem.return.id].properties} /> </TypeContext.Provider>
useMenuSection returns props for a section:
<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useMenuSection.return.id].properties} /> </TypeContext.Provider>
State for the trigger is managed by the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useMenuTriggerState} />
hook from @react-stately/menu. State for the menu itself is managed by the <TypeLink links={treeDocs.links} type={treeDocs.exports.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.
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 <TypeLink links={docs.links} type={docs.exports.useMenuTrigger} /> and <TypeLink links={statelyDocs.links} type={statelyDocs.exports.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 type {MenuTriggerProps} from '@react-stately/menu';
import {useMenuTriggerState} from '@react-stately/menu';
import {useMenuTrigger} from '@react-aria/menu';
import {Item} from '@react-stately/collections';
// Reuse the Popover, and Button from your component library. See below for details.
import {Popover, Button} from 'your-component-library';
interface MenuButtonProps<T> extends AriaMenuProps<T>, MenuTriggerProps {
label?: string
}
function MenuButton<T extends object>(props: MenuButtonProps<T>) {
// Create state based on the incoming props
let state = useMenuTriggerState(props);
// Get props for the button and menu elements
let ref = React.useRef(null);
let {menuTriggerProps, menuProps} = useMenuTrigger<T>({}, state, ref);
return (
<>
<Button
{...menuTriggerProps}
buttonRef={ref}
style={{height: 30, fontSize: 14}}>
{props.label}
<span aria-hidden="true" style={{paddingLeft: 5}}>▼</span>
</Button>
{state.isOpen &&
<Popover state={state} triggerRef={ref} placement="bottom start">
<Menu
{...props}
{...menuProps} />
</Popover>
}
</>
);
}
Next, let's implement the Menu component. This will appear inside the Popover when the user presses the button.
It is built using the <TypeLink links={menuDocs.links} type={menuDocs.exports.useMenu} /> and <TypeLink links={treeDocs.links} type={treeDocs.exports.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 type {AriaMenuProps} from '@react-aria/menu';
import {useMenu} from '@react-aria/menu';
import {useTreeState} from '@react-stately/tree';
function Menu<T extends object>(props: AriaMenuProps<T>) {
// Create menu state based on the incoming props
let state = useTreeState(props);
// Get props for the menu element
let ref = React.useRef(null);
let {menuProps} = useMenu(props, state, ref);
return (
<ul {...menuProps} ref={ref}>
{[...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 <TypeLink links={menuDocs.links} type={menuDocs.exports.useMenuItem} />,
and the state object passed via props from Menu.
import {useMenuItem} from '@react-aria/menu';
function MenuItem({item, state}) {
// Get props for the menu item element
let ref = React.useRef(null);
let {menuItemProps, isSelected} = useMenuItem({key: item.key}, state, ref);
return (
<li {...menuItemProps} ref={ref}>
{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>
[role=menu] {
margin: 0;
padding: 0;
list-style: none;
width: 200px;
}
[role=menuitem],
[role=menuitemradio],
[role=menuitemcheckbox] {
padding: 2px 5px;
outline: none;
cursor: default;
display: flex;
justify-content: space-between;
color: black;
&:focus {
background: gray;
color: white;
}
&[aria-disabled] {
color: gray;
}
}
The Popover component is used to contain the menu.
It can be shared between many other components, including ComboBox,
Select, and others.
See usePopover for more examples of popovers.
import type {AriaPopoverProps} from 'react-aria';
import type {OverlayTriggerState} from 'react-stately';
import {usePopover, Overlay, DismissButton} from '@react-aria/overlays';
interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> {
children: React.ReactNode,
state: OverlayTriggerState
}
function Popover({children, state, ...props}: PopoverProps) {
let popoverRef = React.useRef(null);
let {popoverProps, underlayProps} = usePopover({
...props,
popoverRef
}, state);
return (
<Overlay>
<div {...underlayProps} style={{position: 'fixed', inset: 0}} />
<div
{...popoverProps}
ref={popoverRef}
style={{
...popoverProps.style,
background: 'lightgray',
border: '1px solid gray'
}}>
<DismissButton onDismiss={state.close} />
{children}
<DismissButton onDismiss={state.close} />
</div>
</Overlay>
);
}
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.
import {useButton} from '@react-aria/button';
function Button(props) {
let ref = props.buttonRef;
let {buttonProps} = useButton(props, ref);
return <button {...buttonProps} ref={ref} style={props.style}>{props.children}</button>;
}
<ExampleCard url="https://codesandbox.io/s/awesome-boyd-c0gbv5?file=/src/Menu.tsx" preview={tailwindPreview} title="Tailwind CSS" description="An example of styling a Menu with Tailwind." />
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>
);
}
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.
import type {Selection} from 'react-stately';
function Example() {
let [selected, setSelected] = React.useState<Selection>(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 === 'all' ? 'all' : [...selected].join(', ')}</p>
</>
);
}
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 <TypeLink links={docs.links} type={docs.exports.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} from '@react-aria/menu';
import {useSeparator} from '@react-aria/separator';
function MenuSection({section, state}) {
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} />
)}
</ul>
</li>
</>;
}
With this in place, we can now render a static menu with multiple sections:
import {Section} from '@react-stately/collections';
<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>
The above example shows sections with static items. Sections can also be populated from a hierarchical data structure.
Similarly to the props on Menu, <Section> takes an array of data using the items prop.
import type {Selection} from 'react-stately';
function Example() {
let [selected, setSelected] = React.useState<Selection>(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>
);
}
Sections without a title must provide an aria-label for accessibility.
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 <TypeLink links={docs.links} type={docs.exports.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 <TypeLink links={docs.links} type={docs.exports.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).
///- begin collapse -///
function MenuButton(props) {
let state = useMenuTriggerState(props);
let ref = React.useRef(null);
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 state={state} triggerRef={ref} placement="bottom start">
<Menu
{...props}
{...menuProps} />
</Popover>
}
</div>
);
}
function Menu(props) {
let state = useTreeState(props);
let ref = React.useRef(null);
let {menuProps} = useMenu(props, state, ref);
return (
<ul {...menuProps} ref={ref}>
{[...state.collection].map(item => (
<MenuItem key={item.key} item={item} state={state} />
))}
</ul>
);
}
///- end collapse -///
function MenuItem({item, state}) {
// Get props for the menu item element and child elements
let ref = React.useRef(null);
let {
menuItemProps,
labelProps,
descriptionProps,
keyboardShortcutProps
} = 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}>
<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>
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>
By default, interacting with an item in a Menu triggers onAction and optionally onSelectionChange depending on the selectionMode. Alternatively, items may be links to another page or website. This can be achieved by passing the href prop to the <Item> component. Link items in a menu are not selectable.
This example shows how to update the MenuItem component with support for rendering an <a> element if an href prop is passed to the item. Note that you'll also need to render the Menu as a <div> instead of a <ul>, since an <a> inside a <ul> is not valid HTML.
///- begin collapse -///
function MenuButton(props) {
let state = useMenuTriggerState(props);
let ref = React.useRef(null);
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 state={state} triggerRef={ref} placement="bottom start">
<Menu
{...props}
{...menuProps} />
</Popover>
}
</div>
);
}
function Menu(props) {
let state = useTreeState(props);
let ref = React.useRef(null);
let {menuProps} = useMenu(props, state, ref);
return (
<div {...menuProps} ref={ref}>
{[...state.collection].map(item => (
<MenuItem key={item.key} item={item} state={state} />
))}
</div>
);
}
///- end collapse -///
function MenuItem({item, state}) {
// Get props for the menu item element and child elements
let ref = React.useRef(null);
let {menuItemProps} = useMenuItem({key: item.key}, state, ref);
/*- begin highlight -*/
let ElementType: React.ElementType = item.props.href ? 'a' : 'div';
/*- end highlight -*/
return (
<ElementType {...menuItemProps} ref={ref}>
{item.rendered}
</ElementType>
);
}
<MenuButton label="Links">
<Item href="https://adobe.com/" target="_blank">Adobe</Item>
<Item href="https://apple.com/" target="_blank">Apple</Item>
<Item href="https://google.com/" target="_blank">Google</Item>
<Item href="https://microsoft.com/" target="_blank">Microsoft</Item>
</MenuButton>
[role=menuitem][href] {
cursor: pointer;
text-decoration: none;
}
The <Item> component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the <TypeLink links={utilsDocs.links} type={utilsDocs.exports.RouterProvider} /> component at the root of your app. See the framework setup guide to learn how to set this up.
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>
</>
);
}
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.
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.