Skip to content

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/react ContextMenu primitives with proper ARIA roles
  • The trigger uses display: contents so 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