A menu that appears at the pointer on right click or long press, built on Base UI ContextMenu with Dropdown Menu's rows.
When to use
- Shortcuts to actions on things people point at: files in a grid, cards on a board, rows in a table, a canvas.
- Power-user paths in tools where right click is expected (editors, file managers, design tools).
- Touch apps that offer press-and-hold actions on an item.
When not to use
- As the only way to reach an action: it is invisible until someone tries it. Offer the same actions in a visible Dropdown Menu or toolbar too.
- Keyboard-first flows: a context region isn't focusable; use a Dropdown Menu on a focusable button.
- Rich content: use Popover.
Installation
npx shadcn add https://eased.dev/r/context-menu.jsonThe command adds Context Menu, everything it builds on and Eased’s tokens to your project.
Installs Dropdown Menu too: the rows come from it.
Usage
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger,} from "@/components/eased/context-menu/context-menu";export function FileTile({ file }: { file: AssetFile }) { return ( <ContextMenu> <ContextMenuTrigger className="rounded-control p-1.5 data-popup-open:bg-surface-active"> <Thumbnail file={file} /> <span className="truncate text-ui-sm">{file.name}</span> </ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem icon={<OpenIcon />} shortcut="⌘O" onClick={() => open(file)}> Open </ContextMenuItem> <ContextMenuItem icon={<DuplicateIcon />} shortcut="⌘D" onClick={() => duplicate(file)}> Duplicate </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem icon={<TrashIcon />} shortcut="⌘⌫" tone="danger" onClick={() => trash(file)}> Move to Trash </ContextMenuItem> </ContextMenuContent> </ContextMenu> );}Examples
A submenu of toggles
Checkbox rows keep the menu open, so several tags can be set in one visit.
<ContextMenuSub> <ContextMenuSubTrigger icon={<TagIcon />}>Tags</ContextMenuSubTrigger> <ContextMenuSubContent> {tags.map((tag) => ( <ContextMenuCheckboxItem key={tag} checked={file.tags.includes(tag)} onCheckedChange={(on) => setTag(file, tag, on)}> {tag} </ContextMenuCheckboxItem> ))} </ContextMenuSubContent></ContextMenuSub>One menu for many items
One ContextMenu per item is simplest and tells each menu which item it is for. A trigger around the whole grid also works; read the item from the event target in onOpenChange.
API reference
ContextMenu
Base UI ContextMenu.Root: open, defaultOpen, onOpenChange, disabled, loopFocus, actionsRef and the rest. It provides the context rows use to blink before closing; your actionsRef still receives close and unmount.
ContextMenuTrigger
Base UI ContextMenu.Trigger props: a <div> region (render for another element).
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | "" | The region's classes. Style its open state with data-popup-open:. |
ContextMenuContent
| Prop | Type | Default | Description |
|---|---|---|---|
finalFocus | Base UI finalFocus | Where focus goes on close. | |
container | HTMLElement | null | document.body | Portal target. |
zIndex | number | 50 | |
width | string | fits the rows, ≥ 192px | A CSS width. |
className | string | "" | Appended to the popup's classes. |
Rows
ContextMenuItem, ContextMenuLinkItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent, ContextMenuGroup, ContextMenuLabel and ContextMenuSeparator are Dropdown Menu's parts under these names (Base UI's ContextMenu re-exports Menu's parts). Their props (icon, shortcut, description, tone, closeOnClick, …) are in Dropdown Menu.
Keyboard
| Key | Action |
|---|---|
ArrowUp / ArrowDown | Moves the highlight; loops at the ends. |
| A letter | Jumps to the next row starting with it. |
Enter / Space | Chooses the row. |
ArrowRight / ArrowLeft | Opens a submenu / closes it. |
Escape | Closes the menu. |
The region itself is opened by pointer (right click) or touch (long press); make the same actions reachable by keyboard elsewhere.
Accessibility
- The popup is a
menuwithmenuitem,menuitemcheckboxandmenuitemradiorows; focus moves into it on open. - Shortcuts are read by name; icons and checks are hidden from assistive tech.
- On touch, a 500ms press and hold opens it at the finger; moving more than 10px first cancels it, so the region still scrolls. Base UI suppresses the iOS callout, and the region's text is unselectable on coarse pointers so the press doesn't start a selection.
- Rows are 32px, 40px on coarse pointers.
Motion
None on the way in (rule 84: right click is a high-frequency action, and the menu should already be there when the button comes up): it appears with its corner just beside the pointer, level with the first row, and its transform-origin at the pointer. It fades out in 100ms. Inside, everything is Dropdown Menu's: one highlight gliding between rows on the snappy spring, a 40ms-off, 40ms-on blink on the chosen command before it closes, and submenus that slide 4px out beside their row. Reduced motion keeps the 100ms fade, jumps the highlight and closes without the blink.
Notes for AI
Use ContextMenu to add right-click and long-press access to actions that already exist in a visible menu or toolbar; wrap each item (file tile, card, row) in its own ContextMenu so the handlers close over that item. Give the region a data-popup-open: fill so people see which item the menu belongs to.
Don't listen for contextmenu or touch timers yourself, and don't position the popup: Base UI places it at the pointer and handles long press. Don't add an enter animation; it is deliberately instant.
Related
- Dropdown Menu: the same rows, opened from a button.
- Popover: an anchored panel for content.