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.
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.
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
ContextMenu | ContextMenuPrimitive.Root | — | Wraps the trigger and content. Open state is always internal — the menu opens where the pointer is. |
ContextMenuTrigger | ContextMenuPrimitive.Trigger | — | The region that opens the menu on right-click or long-press. Pass className to style the box. |
ContextMenuContent | ContextMenuPrimitive.Content | — | The floating surface, portalled to document.body. Holds the menu items. |
ContextMenuPortal | ContextMenuPrimitive.Portal | — | Escape hatch for portalling content yourself — Content and SubContent already portal, so you rarely need it. |
ContextMenuGroup | ContextMenuPrimitive.Group | — | Groups related items so assistive tech announces them together. |
ContextMenuItem | ContextMenuPrimitive.Item | — | A selectable action row. See the item props below. |
ContextMenuCheckboxItem | ContextMenuPrimitive.CheckboxItem | — | Toggle row with a check indicator. Controlled only — checked (default false) plus onCheckedChange; there is no defaultChecked. |
ContextMenuRadioGroup | ContextMenuPrimitive.RadioGroup | — | Groups radio items and tracks the selected value. Controlled only — value plus onValueChange; there is no defaultValue. |
ContextMenuRadioItem | ContextMenuPrimitive.RadioItem | — | Single-choice row with a dot indicator; requires a unique value. |
ContextMenuLabel | ContextMenuPrimitive.Label | — | Non-interactive group heading. Accepts inset. |
ContextMenuSeparator | ContextMenuPrimitive.Separator | — | Full-bleed divider drawn with the decorative border token. |
ContextMenuShortcut | span | — | Muted, right-aligned shortcut text. Use KeyboardHint from the same entry when the combo contains a modifier. |
ContextMenuSub | ContextMenuPrimitive.Sub | — | Wraps a nested submenu (SubTrigger + SubContent). |
ContextMenuSubTrigger | ContextMenuPrimitive.SubTrigger | — | Row that opens a submenu; accepts inset and appends a chevron. |
ContextMenuSubContent | ContextMenuPrimitive.SubContent | — | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
modal | boolean | true | Whether the menu blocks interaction with the rest of the page while open. |
onOpenChange | (open: boolean) => void | — | Called 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.
| Prop | Type | Default | Description |
|---|---|---|---|
alignOffset | number | 0 | Offset from the pointer position along the alignment axis, in pixels. |
avoidCollisions | boolean | true | Flips or shifts the panel to keep it inside the viewport. |
collisionPadding | number | Partial<Record<Side, number>> | 0 | Space to keep between the panel and the viewport edge, either uniform or per side. |
loop | boolean | false | Whether keyboard navigation wraps from the last item back to the first. |
className | string | — | Additional Tailwind classes — commonly a width such as w-52. |
ContextMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'destructive' | 'default' | Styles the row — destructive paints the label, icon and focus tint in the destructive ink token. |
inset | boolean | false | Adds left padding to align with items that have a leading icon or indicator. |
disabled | boolean | false | Disables the item, removing pointer events and dimming it. |
onSelect | (event: Event) => void | — | Called when the item is selected via pointer or keyboard. |
textValue | string | — | Text used for typeahead matching when the item contains more than plain text. |
asChild | boolean | false | Merges 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, andmenuitemradioroles; 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 aDropdownMenu— 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 tobg-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.