Context Menu
A menu opened at the pointer by a right-click, a long press or Shift+F10, with Menu's items, headers, dividers and submenus.
Overview
ContextMenu is the region a context menu belongs to: a right-click, a long press or Shift+F10
anywhere in it opens the menu at that point, in place of the browser's own. The menu is a
Menu's list, MenuList, with the same items, headers,
dividers and submenus, so everything on that page about menu content applies here; only the trigger
and the position are different. The list is portaled out of the region and positioned with
React Aria's useOverlayPosition, its start corner
at the pointer, and flipped above the pointer when there is no room below.
Basic usage
Render the region's own content and a MenuList as the children of ContextMenu. The region is a
div by default; component and asChild change that, and className, style and the other
attributes go to it.
import { ContextMenu, MenuDivider, MenuItem, MenuList } from '@chassis-ui/react'
export const Example = () => (
<ContextMenu className="border border-style-dashed rounded p-xl text-center" tabIndex={0}>
Right-click, long-press, or press Shift+F10 in this area.
<MenuList>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
<MenuDivider />
<MenuItem>Delete</MenuItem>
</MenuList>
</ContextMenu>
)Opening the menu
The menu opens on:
- A right-click, or Ctrl+click on macOS: the
contextmenuevent, whose default the region prevents so the browser's menu stays closed. - A long press by a finger or a pen, after half a second without moving.
- Shift+F10 or the context menu key on an element inside the region, or on the region itself.
The menu opens under that element. Nothing focusable, nothing to press the key on: give a
region of plain content
tabIndex={0}, as the examples on this page do.
The first item takes focus when the menu opens, so the arrow keys, Home and End move through the items at once. A second right-click while the menu is open moves it to the new point.
Set disabled to leave the region to the browser: a right-click shows the browser's own menu, and
a long press and the keys do nothing.
Items
Everything Menu renders in its list renders here: MenuItem with icon and description,
MenuHeader, MenuDivider, MenuText, MenuSubmenu, and the items prop of MenuList for a
data-driven list.
import {
ContextMenu,
Icon,
MenuDivider,
MenuHeader,
MenuItem,
MenuList,
MenuSubmenu
} from '@chassis-ui/react'
export const Example = () => (
<ContextMenu className="border border-style-dashed rounded p-xl text-center" tabIndex={0}>
Report.pdf
<MenuList aria-label="Actions for Report.pdf">
<MenuHeader>Report.pdf</MenuHeader>
<MenuItem icon={<Icon name="eye-outline" size={16} />} description="Read-only">
Open
</MenuItem>
<MenuItem icon={<Icon name="gear-outline" size={16} />}>Properties</MenuItem>
<MenuSubmenu trigger="Share">
<MenuItem icon={<Icon name="users-outline" size={16} />}>With the team</MenuItem>
<MenuItem>Copy link</MenuItem>
</MenuSubmenu>
<MenuDivider />
<MenuItem disabled>Rename</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuList>
</ContextMenu>
)One menu per row
A list of rows with a menu each is a ContextMenu per row. The example renders each as the
<li> of a .list with component; asChild fits an existing row component the same way. Name
each list after its row with aria-label on the MenuList (see Accessibility below).
import { ContextMenu, MenuDivider, MenuItem, MenuList } from '@chassis-ui/react'
const files = ['Report.pdf', 'Budget.xlsx', 'Notes.md']
export const Example = () => (
<ul className="list">
{files.map((file) => (
<ContextMenu key={file} component="li" className="list-item" tabIndex={0}>
{file}
<MenuList aria-label={`Actions for ${file}`}>
<MenuItem>Open</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuDivider />
<MenuItem>Delete</MenuItem>
</MenuList>
</ContextMenu>
))}
</ul>
)Closing the menu
By default a click on an item and a press outside the menu close it; both are requests to
onVisibleChange. A press rather than a click outside, as a native menu closes, so the release
after a long press doesn't close what the press opened. The Escape key closes the menu whatever
autoClose says, and so does the closing of a Modal or Drawer around the region.
autoClose narrows this as it does on Menu: 'inside' closes only on an item, 'outside' only
on a press outside, and false on neither. A form control inside the list (MenuText with an
input) is used in place and never closes the menu.
Controlled
visible and onVisibleChange make the menu controlled, as on every component that opens: the
menu shows and hides only when visible changes, and reports each request to open or close.
defaultVisible opens an uncontrolled menu at first, under the region's start edge, since no
pointer has asked for it anywhere yet.
import { useState } from 'react'
import { Button, ContextMenu, MenuItem, MenuList } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<div className="d-flex flex-column gap-sm">
<ContextMenu
className="border border-style-dashed rounded p-xl text-center"
tabIndex={0}
visible={visible}
onVisibleChange={setVisible}
>
Right-click in this area.
<MenuList>
<MenuItem>Rename</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Move to</MenuItem>
</MenuList>
</ContextMenu>
<div className="d-flex align-items-center gap-sm">
<Button variant="outline" onClick={() => setVisible(false)}>
Close from here
</Button>
<span>The menu is {visible ? 'open' : 'closed'}.</span>
</div>
</div>
)
}Accessibility
- The list has
role="menu"and is labelled by the region, the way aMenu's list is labelled by its toggle, so a screen reader announces the region's text as the menu's name. When the region holds more than a name, give theMenuListanaria-labelof its own instead, which replaces that label:aria-label="Actions for Report.pdf". - The region carries no
aria-haspopuporaria-expanded: both are invalid on an element with no widget role, and a region is any element. A control inside the region that always opens the menu, such as a "more" button, is better served by aMenu. - Escape, and a click on an item, return focus to the element that had it when the menu opened. A press outside leaves focus where the press puts it.
- The keyboard needs something to focus inside the region: a row that is a link or a button, or
tabIndex={0}on the region itself.
API
ContextMenu
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
autoClose? | MenuAutoClose | — |
Controls which presses close the menu. true closes on a click on an item and on a press
outside the menu. false leaves closing to the Escape key and visible. 'inside' closes
only on a click on an item. 'outside' closes only on a press outside the menu. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
component? | ElementType | 'div' |
| Component used for the root node, the region the menu opens from. Either a string to use an HTML element or a component. | ||
defaultVisible? | boolean | — |
Whether the menu is open when it first renders. Use it instead of visible when nothing
outside needs to control the menu. Opened this way, the menu sits under the region's start
edge, since no pointer has asked for it anywhere. | ||
disabled? | boolean | — |
| Leaves the region to the browser: a right-click shows the browser's own menu, a long press and the keys do nothing. | ||
onHide? | (() => void) | — |
| Callback fired when the menu hides. | ||
onShow? | (() => void) | — |
| Callback fired when the menu shows. | ||
onVisibleChange? | ((visible: boolean) => void) | — |
Callback fired when the menu asks to show or hide: a right-click, a long press or Shift+F10
in the region, a click on an item, a press outside, the Escape key, or the closing of the
dialog it is in. Receives the state it asks for. With visible set, the menu changes only
when visible does. | ||
ref? | any | — |
visible? | boolean | — |
Whether the menu is open. Setting it makes the menu controlled: it shows and hides only when
this changes, so pair it with onVisibleChange. Leave it unset, or use defaultVisible, for
a menu that opens and closes itself. | ||
The list and its content take MenuList, MenuItem, MenuHeader, MenuDivider, MenuText and
MenuSubmenu, documented on the Menu page.