ContextMenu
Right-click context menu that wraps any element and opens a native-feeling menu
on right-click or long-press. Compose the trigger area and content as children,
reuse the shared Menu.* item primitives, or drop in a fully custom panel
(tabs, search, anything). Built on @base-ui/react ContextMenu primitives.
Live Preview
Import
import { ContextMenu, Menu } from 'entangle-ui';Usage
<ContextMenu> <ContextMenu.Trigger> <div className="editor-viewport">Right-click anywhere in this area</div> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.Item shortcut="⌘X" onClick={handleCut}> Cut </Menu.Item> <Menu.Item shortcut="⌘C" onClick={handleCopy}> Copy </Menu.Item> <Menu.Item shortcut="⌘V" onClick={handlePaste}> Paste </Menu.Item> </ContextMenu.Content></ContextMenu>Items reuse the same Menu.Item, Menu.Group, Menu.Separator,
Menu.RadioGroup, Menu.CheckboxItem, and submenu primitives documented on the
Menu page.
Per-Area Menus
There is no config resolver. To give different areas different menus, give each
area its own ContextMenu with its own content — the menu is defined right
where the area lives, not branched inside a function.
<> <ContextMenu> <ContextMenu.Trigger> <Canvas /> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.Item onClick={addNode}>Add Node</Menu.Item> <Menu.Item onClick={paste}>Paste</Menu.Item> </ContextMenu.Content> </ContextMenu>
<ContextMenu> <ContextMenu.Trigger> <NodeCard node={node} /> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.Item onClick={() => rename(node)}>Rename</Menu.Item> <Menu.Item disabled={node.locked} onClick={() => remove(node)}> Delete </Menu.Item> </ContextMenu.Content> </ContextMenu></>When rendering a list, map each item to its own ContextMenu:
{ items.map(item => ( <ContextMenu key={item.id}> <ContextMenu.Trigger> <div>{item.name}</div> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.Item onClick={() => inspect(item)}>Inspect {item.name}</Menu.Item> <Menu.Item disabled={item.locked} onClick={() => remove(item)}> Delete </Menu.Item> </ContextMenu.Content> </ContextMenu> ));}Per-area menus
Custom Panels
ContextMenu.Content accepts any node. The component manages opening, closing,
and positioning while you render whatever the panel needs — tabs, a search
field, a color grid, etc.
<ContextMenu> <ContextMenu.Trigger> <NodeCanvas /> </ContextMenu.Trigger> <ContextMenu.Content> <Tabs defaultValue="add"> <TabList> <Tab value="add">Add</Tab> <Tab value="recent">Recent</Tab> </TabList> <TabPanel value="add"> <Input placeholder="Search nodes…" /> {/* custom node grid */} </TabPanel> <TabPanel value="recent">{/* recent nodes */}</TabPanel> </Tabs> </ContextMenu.Content></ContextMenu>Advanced node picker (tabs + search + grid)
Selection States
ContextMenu reuses the Menu selection primitives.
const [mode, setMode] = useState('edit');
<ContextMenu> <ContextMenu.Trigger> <div>Right-click for mode selection</div> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.RadioGroup value={mode} onValueChange={setMode}> <Menu.RadioItem value="edit">Edit Mode</Menu.RadioItem> <Menu.RadioItem value="object">Object Mode</Menu.RadioItem> <Menu.RadioItem value="sculpt">Sculpt Mode</Menu.RadioItem> </Menu.RadioGroup> </ContextMenu.Content></ContextMenu>;Selection states
Nested Submenus
<ContextMenu.Content> <Menu.Sub> <Menu.SubTrigger>Add Object</Menu.SubTrigger> <Menu.SubContent> <Menu.Item onClick={addCube}>Cube</Menu.Item> <Menu.Item onClick={addSphere}>Sphere</Menu.Item> <Menu.Item onClick={addPlane}>Plane</Menu.Item> </Menu.SubContent> </Menu.Sub></ContextMenu.Content>Nested submenus
Disabled
<ContextMenu disabled> <ContextMenu.Trigger> <div>Context menu is disabled here</div> </ContextMenu.Trigger> <ContextMenu.Content> <Menu.Item>Action</Menu.Item> </ContextMenu.Content></ContextMenu>API
ContextMenu
The root. Owns open/close state for one trigger area.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | ContextMenu.Trigger and ContextMenu.Content. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | — | Uncontrolled initial open state. |
onOpenChange | (open: boolean) => void | — | Called when the menu opens or closes. |
disabled | boolean | false | Disables opening the context menu. |
gap | number | 8 | Gap in px between submenu popups and their anchor, inherited by any Menu.SubContent inside the content. |
ref | Ref<MenuHandle> | — | Imperative handle. Call ref.current.close() to close the menu from app code. |
ContextMenu.Trigger
The right-click area. By default the children are wrapped in a display: contents element so the trigger adds no extra box. Pass render to make the
trigger render as your own element instead — no wrapper, fully stylable.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The right-click target area. |
render | ReactElement | — | Render the trigger as this element instead of wrapping the children in a display: contents element. The element carries its own children. |
className | string | — | Additional CSS class names. |
style | CSSProperties | — | Inline styles (merged over the default display: contents). |
ContextMenu.Content
The positioned popup surface, placed at the pointer. Place items or any custom node inside.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Items, groups, or custom panel content. |
className | string | — | Additional CSS class names for the popup. |
style | CSSProperties | — | Inline styles for the popup. |
testId | string | — | Test identifier for automated testing. |
Accessibility
- Built on
@base-ui/reactContextMenu primitives with proper ARIA roles - The trigger uses
display: contentsso it adds no extra DOM node - Full keyboard navigation inside the menu: Arrow Up/Down, Enter, Escape
- Radio groups use proper radio semantics
- Focus is managed automatically when the context menu opens and closes