Skip to main contentSkip to docs navigation

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.

Right-click, long-press, or press Shift+F10 in this area.
JSX
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 contextmenu event, 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.

Report.pdf
JSX
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).

  • Report.pdf
  • Budget.xlsx
  • Notes.md
JSX
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.

Right-click in this area.
The menu is closed.
JSX
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 a Menu'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 the MenuList an aria-label of its own instead, which replaces that label: aria-label="Actions for Report.pdf".
  • The region carries no aria-haspopup or aria-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 a Menu.
  • 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

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