Components

Context Menu

A menu of actions revealed by right-clicking (or long-pressing) a target region, built on Radix with full keyboard navigation and nested submenus.

Basic usage

Right-click the dashed box to open the menu.

The item structure mirrors DropdownMenu exactly — same variant and inset props, same spacing tokens — so the two menus read as one surface. Because a right-click gesture does not exist on touch devices, treat this as an accelerator only: every action offered here needs a second route, typically an button that opens the same list as a DropdownMenu.

Right-click here

Checkboxes, radios & submenus

Compose ContextMenuCheckboxItem, ContextMenuRadioGroup, and ContextMenuSub for richer menus.

The two selection rows are controlled only. The Radix menu primitives read checked (defaulting to false) and value; unlike the standalone Checkbox and RadioGroup they have no defaultChecked or defaultValue. Passing one type-checks but lands on the underlying <div>, so the row never ticks — hold the state yourself.

The package renders ContextMenuSubContent inside its own portal. ContextMenuContent carries overflow-x-hidden overflow-y-auto so a long menu can scroll, and its open animation leaves a transform behind, which makes the panel a containing block even for position: fixed children. Without the portal the submenu opens beside the parent panel and gets clipped away — the chevron row simply does nothing, with no error in the console.

Right-click here

Compact density

Menu rows measure their vertical padding with py-(--menu-item-py) rather than a fixed py-1.5, so they follow the density axis: 6px at the default setting, 4px under data-density="compact". Only the vertical axis moves — compact means more rows on screen, not text jammed against the panel edge. In an app the attribute is set once on <html> by DensityProvider from @comitor/ui/shell; the panel below scopes it by hand so you can compare it with the menus above.

Right-click here

Props

The parts exported from @comitor/ui. Every part forwards the props of the Radix primitive it wraps, plus the native props of the element it renders.

PropTypeDefaultDescription
ContextMenuContextMenuPrimitive.RootWraps the trigger and content. Open state is always internal — the menu opens where the pointer is.
ContextMenuTriggerContextMenuPrimitive.TriggerThe region that opens the menu on right-click or long-press. Pass className to style the box.
ContextMenuContentContextMenuPrimitive.ContentThe floating surface, portalled to document.body. Holds the menu items.
ContextMenuPortalContextMenuPrimitive.PortalEscape hatch for portalling content yourself — Content and SubContent already portal, so you rarely need it.
ContextMenuGroupContextMenuPrimitive.GroupGroups related items so assistive tech announces them together.
ContextMenuItemContextMenuPrimitive.ItemA selectable action row. See the item props below.
ContextMenuCheckboxItemContextMenuPrimitive.CheckboxItemToggle row with a check indicator. Controlled only — checked (default false) plus onCheckedChange; there is no defaultChecked.
ContextMenuRadioGroupContextMenuPrimitive.RadioGroupGroups radio items and tracks the selected value. Controlled only — value plus onValueChange; there is no defaultValue.
ContextMenuRadioItemContextMenuPrimitive.RadioItemSingle-choice row with a dot indicator; requires a unique value.
ContextMenuLabelContextMenuPrimitive.LabelNon-interactive group heading. Accepts inset.
ContextMenuSeparatorContextMenuPrimitive.SeparatorFull-bleed divider drawn with the decorative border token.
ContextMenuShortcutspanMuted, right-aligned shortcut text. Use KeyboardHint from the same entry when the combo contains a modifier.
ContextMenuSubContextMenuPrimitive.SubWraps a nested submenu (SubTrigger + SubContent).
ContextMenuSubTriggerContextMenuPrimitive.SubTriggerRow that opens a submenu; accepts inset and appends a chevron.
ContextMenuSubContentContextMenuPrimitive.SubContentThe submenu panel, portalled so the parent panel cannot clip it.

ContextMenu (root) — note there is no open or defaultOpen: the menu is opened by the pointer gesture and positioned where it happened.

PropTypeDefaultDescription
modalbooleantrueWhether the menu blocks interaction with the rest of the page while open.
onOpenChange(open: boolean) => voidCalled when the menu opens or closes.
dir'ltr' | 'rtl''ltr'Reading direction, affecting submenu placement and arrow keys.

ContextMenuContent side, sideOffset and align are fixed by the primitive, since the panel anchors to the pointer rather than to a trigger.

PropTypeDefaultDescription
alignOffsetnumber0Offset from the pointer position along the alignment axis, in pixels.
avoidCollisionsbooleantrueFlips or shifts the panel to keep it inside the viewport.
collisionPaddingnumber | Partial<Record<Side, number>>0Space to keep between the panel and the viewport edge, either uniform or per side.
loopbooleanfalseWhether keyboard navigation wraps from the last item back to the first.
classNamestringAdditional Tailwind classes — commonly a width such as w-52.

ContextMenuItem

PropTypeDefaultDescription
variant'default' | 'destructive''default'Styles the row — destructive paints the label, icon and focus tint in the destructive ink token.
insetbooleanfalseAdds left padding to align with items that have a leading icon or indicator.
disabledbooleanfalseDisables the item, removing pointer events and dimming it.
onSelect(event: Event) => voidCalled when the item is selected via pointer or keyboard.
textValuestringText used for typeahead matching when the item contains more than plain text.
asChildbooleanfalseMerges the item props onto its single child — use it to render a link as a menu item.

Accessibility

  • The menu opens on the platform context-menu gesture — right-click on pointer devices and long-press on touch.
  • Once open, focus is trapped inside the menu and arrow keys move between items, with type-ahead to jump by first letter.
  • Enter or Space activate the focused item; Esc closes the menu and restores focus to the trigger.
  • Items carry the correct menuitem, menuitemcheckbox, and menuitemradio roles; disabled items are skipped during keyboard navigation.
  • A right-click is not a gesture every input device has, so nothing may live only here. Mirror the same actions behind a visible control — an button that opens a DropdownMenu — and keep the context menu as the accelerator.
  • variant="destructive" paints the label in the destructive ink step, not the destructive fill. The fill is a button background pinned to one value across both themes and only reaches 3.53:1 as text on the popover surface — worse on focus, where the row tints to bg-destructive/20 (2.98:1). The ink step clears both themes and both backgrounds: 7.15:1 / 6.18:1 in light at rest and on focus, 6.42:1 / 5.42:1 in dark.