import {ActionMenu} from '@primer/react'
ActionMenu ships with ActionMenu.Button which is an accessible trigger for the overlay. It's recommended to compose ActionList with ActionMenu.Overlay
<ActionMenu><ActionMenu.Button>Menu</ActionMenu.Button><ActionMenu.Overlay><ActionList><ActionList.Item onSelect={event => console.log('New file')}>New file</ActionList.Item><ActionList.Item>Copy link</ActionList.Item><ActionList.Item>Edit file</ActionList.Item><ActionList.Divider /><ActionList.Item variant="danger">Delete file</ActionList.Item></ActionList></ActionMenu.Overlay></ActionMenu>
You can choose to have a different anchor for the Menu dependending on the application's context.
<ActionMenu><ActionMenu.Anchor><IconButton icon={KebabHorizontalIcon} variant="invisible" aria-label="Column options" /></ActionMenu.Anchor><ActionMenu.Overlay><ActionList><ActionList.Item><ActionList.LeadingVisual><PencilIcon /></ActionList.LeadingVisual>Rename</ActionList.Item><ActionList.Item><ActionList.LeadingVisual><ArchiveIcon /></ActionList.LeadingVisual>Archive all cards</ActionList.Item><ActionList.Item variant="danger"><ActionList.LeadingVisual><TrashIcon /></ActionList.LeadingVisual>Delete</ActionList.Item></ActionList></ActionMenu.Overlay></ActionMenu>
<ActionMenu><ActionMenu.Button>Open column menu</ActionMenu.Button><ActionMenu.Overlay><ActionList showDividers><ActionList.Group title="Live query"><ActionList.Item><ActionList.LeadingVisual><SearchIcon /></ActionList.LeadingVisual>repo:github/memex,github/github</ActionList.Item></ActionList.Group><ActionList.Divider /><ActionList.Group title="Layout" variant="subtle"><ActionList.Item><ActionList.LeadingVisual><NoteIcon /></ActionList.LeadingVisual>Table<ActionList.Description variant="block">Information-dense table optimized for operations across teams</ActionList.Description></ActionList.Item><ActionList.Item role="listitem"><ActionList.LeadingVisual><ProjectIcon /></ActionList.LeadingVisual>Board<ActionList.Description variant="block">Kanban-style board focused on visual states</ActionList.Description></ActionList.Item></ActionList.Group><ActionList.Divider /><ActionList.Group><ActionList.Item><ActionList.LeadingVisual><FilterIcon /></ActionList.LeadingVisual>Save sort and filters to current view</ActionList.Item><ActionList.Item><ActionList.LeadingVisual><FilterIcon /></ActionList.LeadingVisual>Save sort and filters to new view</ActionList.Item></ActionList.Group><ActionList.Divider /><ActionList.Group><ActionList.Item><ActionList.LeadingVisual><GearIcon /></ActionList.LeadingVisual>View settings</ActionList.Item></ActionList.Group></ActionList></ActionMenu.Overlay></ActionMenu>
Use selectionVariant on ActionList to create a menu with single or multiple selection.
const fieldTypes = [{icon: TypographyIcon, name: 'Text'},{icon: NumberIcon, name: 'Number'},{icon: CalendarIcon, name: 'Date'},{icon: SingleSelectIcon, name: 'Single select'},{icon: IterationsIcon, name: 'Iteration'}]const Example = () => {const [selectedIndex, setSelectedIndex] = React.useState(1)const selectedType = fieldTypes[selectedIndex]return (<><Heading as="h2" sx={{fontSize: 1, mb: 2}}>Field type:</Heading><ActionMenu><ActionMenu.Button aria-label={`Field type: ${selectedType.name}`} leadingIcon={selectedType.icon}>{selectedType.name}</ActionMenu.Button><ActionMenu.Overlay width="medium"><ActionList selectionVariant="single" aria-label="Field type">{fieldTypes.map((type, index) => (<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}><ActionList.LeadingVisual><type.icon /></ActionList.LeadingVisual>{type.name}</ActionList.Item>))}</ActionList></ActionMenu.Overlay></ActionMenu></>)}render(<Example />)
To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass it as anchorRef to ActionMenu. Make sure you add aria-expanded and aria-haspopup to the external anchor.
Make sure to add aria-label to ActionList with the purpose of the menu, example: "File options".
const Example = () => {const [open, setOpen] = React.useState(false)const anchorRef = React.createRef()return (<><Button ref={anchorRef} aria-haspopup="true" aria-expanded={open} onClick={() => setOpen(!open)}>{open ? 'Close Menu' : 'Open Menu'}</Button><ActionMenu aria-label="File options" open={open} onOpenChange={setOpen} anchorRef={anchorRef}><ActionMenu.Overlay><ActionList><ActionList.Item>Copy link</ActionList.Item><ActionList.Item>Quote reply</ActionList.Item><ActionList.Item>Edit comment</ActionList.Item><ActionList.Divider /><ActionList.Item variant="danger">Delete file</ActionList.Item></ActionList></ActionMenu.Overlay></ActionMenu></>)}render(<Example />)
To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass it as anchorRef to ActionMenu:
const handleEscape = () => alert('you hit escape!')render(<ActionMenu><ActionMenu.Button>Actions Menu</ActionMenu.Button><ActionMenu.Overlay width="medium" onEscape={handleEscape}><ActionList><ActionList.Item>Open current Codespace<ActionList.Description variant="block">Your existing Codespace will be opened to its previous state, and you'll be asked to manually switch tonew-branch.</ActionList.Description><ActionList.TrailingVisual>⌘O</ActionList.TrailingVisual></ActionList.Item><ActionList.Item>Create new Codespace<ActionList.Description variant="block">Create a brand new Codespace with a fresh image and checkout this branch.</ActionList.Description><ActionList.TrailingVisual>⌘C</ActionList.TrailingVisual></ActionList.Item></ActionList></ActionMenu.Overlay></ActionMenu>)
For menus with selection, it's common to show the selected value in the button. To give context to the user, it's important to show the purpose of the menu as well.
Visually, this can be an external label above the button or an inline label inside the button. In both cases, the label for screen readers should have both purpose and value, example: "Field type: Number".
If you have an external label, you can achieve that by adding an aria-label to ActionMenu.Button which contains both the purpose and current selected value, example: "Field type: Number". In addition to that, you must also add aria-labelledby to the ActionList with the id of the external label to indicate the purpose of the menu.
Alternatively, you can add an inline label by passing label to ActionMenu.Button instead of aria-label, example: "Field type", which will add the correct aria-label (Field type: Number) on the button and aria-labelledby (Field type) on the menu for you.
const fieldTypes = [{icon: TypographyIcon, name: 'Text'},{icon: NumberIcon, name: 'Number'},{icon: CalendarIcon, name: 'Date'},{icon: SingleSelectIcon, name: 'Single select'},{icon: IterationsIcon, name: 'Iteration'}]const ExternalLabel = () => {const [selectedIndex, setSelectedIndex] = React.useState(1)const selectedType = fieldTypes[selectedIndex]return (<><Heading as="h2" id="purpose" sx={{fontSize: 1, mb: 2}}>Field type</Heading><ActionMenu><ActionMenu.Button aria-label={`Field type: ${selectedType.name}`} leadingIcon={selectedType.icon}>{selectedType.name}</ActionMenu.Button><ActionMenu.Overlay width="medium"><ActionList selectionVariant="single" aria-labelledby="purpose">{fieldTypes.map((type, index) => (<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}><ActionList.LeadingVisual><type.icon /></ActionList.LeadingVisual>{type.name}</ActionList.Item>))}</ActionList></ActionMenu.Overlay></ActionMenu></>)}const InlineLabel = () => {const [selectedIndex, setSelectedIndex] = React.useState(1)const selectedType = fieldTypes[selectedIndex]return (<ActionMenu><ActionMenu.Button label="Field type" leadingIcon={selectedType.icon}>{selectedType.name}</ActionMenu.Button><ActionMenu.Overlay width="medium"><ActionList selectionVariant="single">{fieldTypes.map((type, index) => (<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}><ActionList.LeadingVisual><type.icon /></ActionList.LeadingVisual>{type.name}</ActionList.Item>))}</ActionList></ActionMenu.Overlay></ActionMenu>)}render(<div style={{display: 'flex', alignItems: 'end'}}><div style={{width: '50%'}}><ExternalLabel /></div><div style={{width: '50%'}}><InlineLabel /></div></div>)
| Name | Type | Default | Description |
|---|---|---|---|
| children Required | React.ReactElement[] | Recommended: ActionMenu.Button or ActionMenu.Anchor with ActionMenu.Overlay | |
| open | boolean | false | If defined, will control the open/closed state of the overlay. Must be used in conjuction with onOpenChange. |
| onOpenChange | (open: boolean) => void | If defined, will control the open/closed state of the overlay. Must be used in conjuction with open. | |
| anchorRef | React.RefObject<HTMLElement> | Useful for defining an external anchor |
| Name | Type | Default | Description |
|---|---|---|---|
| children Required | React.ReactElement | ||
| aria-label | string | Label for button, also used to label the menu | |
| label | string | Add an inline label, as a replacement for aria-label | |
Additional props are passed to the <Button> element. See Button docs for a list of props accepted by the <Button> element. If an as prop is specified, the accepted props will change accordingly. | |||
| Name | Type | Default | Description |
|---|---|---|---|
| children Required | React.ReactElement | Accepts a single child element |
| Name | Type | Default | Description |
|---|---|---|---|
| children Required | React.ReactElement | React.ReactElement[] | ||
| align | start | center | end | start | |
Additional props are passed to the <Overlay> element. See Overlay docs for a list of props accepted by the <Overlay> element. | |||
Interface guidelines: Action List + Menu