Context Menu
Actions for a specific element, opened by right-click (or long-press). Built on
Base UI ContextMenu; the popup shares its look and keyboard behavior with
Menu. Because it’s hidden until invoked, a context menu is always a shortcut —
mirror its actions somewhere visible.
Import
import { ContextMenu } from "@photon-ai/pho-ui/components/context-menu";
Basic
ContextMenu.Trigger wraps the area that responds to right-click; the popup
opens at the pointer. The rest mirrors Menu — Items, a Separator, and
color="destructive" for a destructive action.
<ContextMenu.Root>
<ContextMenu.Trigger className="…">Right-click here</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Positioner>
<ContextMenu.Popup>
<ContextMenu.Item>Copy</ContextMenu.Item>
<ContextMenu.Item>Paste</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item color="destructive">Delete</ContextMenu.Item>
</ContextMenu.Popup>
</ContextMenu.Positioner>
</ContextMenu.Portal>
</ContextMenu.Root>
Groups and submenus
Cluster related actions under a ContextMenu.Group with a GroupLabel, and
nest a ContextMenu.SubmenuRoot for a second level. The submenu opens on
hover or Arrow Right and shares the parent’s styling.
<ContextMenu.Popup>
<ContextMenu.Group>
<ContextMenu.GroupLabel>Edit</ContextMenu.GroupLabel>
<ContextMenu.Item>Copy</ContextMenu.Item>
<ContextMenu.Item>Paste</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.SubmenuRoot>
<ContextMenu.SubmenuTrigger>
Share
<CaretRight className="ml-auto" />
</ContextMenu.SubmenuTrigger>
<ContextMenu.Portal>
<ContextMenu.Positioner>
<ContextMenu.Popup>
<ContextMenu.Item>Copy link</ContextMenu.Item>
<ContextMenu.Item>Email</ContextMenu.Item>
</ContextMenu.Popup>
</ContextMenu.Positioner>
</ContextMenu.Portal>
</ContextMenu.SubmenuRoot>
</ContextMenu.Popup>
Anatomy
- ContextMenu.Root — owns open state; opens on the right-click / long-press
- ContextMenu.Trigger — the area that responds to right-click (a
<div>) - ContextMenu.Portal → Positioner → Popup — the floating panel, anchored at the pointer
- ContextMenu.Item — an action; Group / GroupLabel / Separator organize the list
- ContextMenu.SubmenuRoot + .SubmenuTrigger — a nested submenu
- ContextMenu.RadioGroup — owns a selected value (pair with
Menu’sRadioItem)
Props
Every part also accepts className and render — Base UI’s prop for swapping
the element it renders. ContextMenu reuses Menu’s parts, so Item,
Positioner, and the submenu parts take the same props documented on
Menu.
ContextMenu.Root (holds state; renders no element)
- open —
boolean— controlled open state - defaultOpen —
boolean(defaultfalse) — initial open state when uncontrolled - onOpenChange —
(open, details) => void— fires when the menu opens or closes - onOpenChangeComplete —
(open) => void— fires after the open/close animation settles - loopFocus —
boolean(defaulttrue) — wrap arrow-key focus at the ends - orientation —
horizontal·vertical(defaultvertical) — arrow-key axis - disabled —
boolean(defaultfalse) — ignore all interaction - closeParentOnEsc —
boolean(defaultfalse) — in a submenu, Escape closes the whole menu
A context menu is always modal, so — unlike Menu — it has no modal,
openOnHover, or delay prop.
ContextMenu.Trigger (<div>)
- takes only
className/renderplus the div’s native props; getsdata-openwhile its menu is showing
ContextMenu.Positioner (<div>)
- side —
top·right·bottom·left— side of the pointer to place the popup - align —
start·center·end(defaultcenter) — alignment along that side - sideOffset —
number(default0) — gap from the anchor point - alignOffset —
number(default0) — shift along the alignment axis
ContextMenu.Item (<div role="menuitem">)
- onClick —
(event) => void— the action to run - closeOnClick —
boolean(defaulttrue) — close the menu when clicked - disabled —
boolean(defaultfalse) - label —
string— overrides the text used for typeahead matching
Accessibility
- Semantics — the popup is
role="menu"and items arerole="menuitem"; aGroupLabellabels itsGroupviaaria-labelledby. The trigger is a plain region — it carries noaria-haspopup, since a context menu is invoked by right-click rather than announced as a button that opens a menu. - Opening — right-click (the native
contextmenuevent) or long-press on touch. The keyboardMenu/Shift+F10key also firescontextmenu, so keyboard users can open it on the focused element. - Keyboard (once open)
Arrow Up/Arrow Downmove the highlight (looping by default)Arrow Rightopens a submenu;Arrow Leftcloses itEnter/Spaceactivate the highlighted item- Typeahead: typing characters jumps to the matching item
Home/Endjump to the first / last itemEscapecloses the menu (a submenu first, then its parent)
- Focus — opening moves focus into the popup and traps it (a context menu is
modal); closing returns focus to where it was. The highlighted item carries
data-highlightedfor styling.
Best practices
- A context menu must be a shortcut, never the only path — mirror every action
in a visible
Menu, toolbar, or button so it’s discoverable. - Scope items to the right-clicked element, and keep the list short.
- On touch it opens via long-press; make sure the same actions are reachable by tap elsewhere.
- Mark destructive items
color="destructive"and separate them from routine actions so a right-click can’t fat-finger a delete.