Context Menu
A menu opened by right-click or long-press, for actions and settings in context.
Installation
The component is exported from @by/experience-system. Add the package with your package manager:
pnpm add @by/experience-systemIn this monorepo, depend on the workspace package (for example via workspace:* or your catalog) so imports resolve to packages/experience-system.
Composition
Use the following composition to build a ContextMenu:
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuLabel (optional)
├── ContextMenuItem / CheckboxItem / RadioGroup / …
├── ContextMenuSeparator (optional)
└── ContextMenuSub (optional)
├── ContextMenuSubTrigger
└── ContextMenuSubContentContextMenuTrigger wraps the surface that opens the menu on pointer secondary click (right-click) or the platform equivalent. Put commands and groups inside ContextMenuContent. Use ContextMenuSub for nested menus. Behavior follows Radix UI Context Menu.
Usage
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
} from '@by/experience-system';ContextMenu and its parts are client components ('use client'). Use them inside a Client Component or a dynamic import when using the Next.js App Router.
<ContextMenu>
<ContextMenuTrigger className="rounded-md border border-dashed px-4 py-8">Right click</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Action</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Pay attention:
rtl:scale-x-[1] to icons that have an obvious "forward" direction, such as "ArrowUndo" and "ArrowRedo".When to use
- When you want to offer quick access to relevant actions based on the user's current selection or focus.
- When space is limited and secondary actions don't need to be always visible.
When not to use
- When the actions are critical or primary and need to be discoverable without extra clicks.
- When the number of contextual options becomes too large, leading to clutter or overwhelm.
Examples
The live preview at the top of this page uses context-menu-usage (minimal trigger + one item).
Complex content
Items with leading icons, inset alignment for icon-less rows, shortcuts, a nested submenu (including an error-styled row via color="error"), checkbox items, and a radio group with an inset label.
API Reference
Subsection titles name @by/experience-system exports and the Radix part they wrap. Pieces map to Radix UI Context Menu primitives. Styling, density-aware spacing, and icons are provided by @by/experience-system and @by/icons. Adheres to the Menu WAI-ARIA design pattern and uses roving tabindex to move focus among items.
ContextMenu
Root. ContextMenu contains all the parts of a context menu. Exported as ContextMenu; forwards ContextMenu.Root props from radix-ui.
| Prop | Type | Default |
|---|---|---|
dir | enum | No default value |
onOpenChange | function | No default value |
modal | boolean | true |
ContextMenuTrigger
ContextMenuTrigger — the area that opens the context menu. Wrap it around the target you want the menu to open from when right-clicking (or using the relevant keyboard shortcuts).
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
disabled | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
ContextMenuPortal
ContextMenuPortal — when used, portals the content part into the document. In this experience system, container defaults to the element from useContainerElement() (ThemeProvider) instead of document.body.
| Prop | Type | Default |
|---|---|---|
forceMount | boolean | No default value |
container | HTMLElement | From ThemeProvider (see above) |
ContextMenuContent
ContextMenuContent — the component that pops out in an open context menu. This export wraps Portal with the Experience System container so you typically render ContextMenuContent directly (without a separate ContextMenuPortal for the main menu).
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
loop | boolean | false |
onCloseAutoFocus | function | No default value |
onEscapeKeyDown | function | No default value |
onPointerDownOutside | function | No default value |
onFocusOutside | function | No default value |
onInteractOutside | function | No default value |
forceMount | boolean | No default value |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionBoundary | Boundary | [] |
collisionPadding | number | Padding | 0 |
sticky | enum | "partial" |
hideWhenDetached | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-side] | "left" | "right" | "bottom" | "top" |
[data-align] | "start" | "end" | "center" |
| CSS variable | Description |
|---|---|
--radix-context-menu-content-transform-origin | The transform-origin computed from the content and arrow positions/offsets |
--radix-context-menu-content-available-width | The remaining width between the trigger and the boundary edge |
--radix-context-menu-content-available-height | The remaining height between the trigger and the boundary edge |
--radix-context-menu-trigger-width | The width of the trigger |
--radix-context-menu-trigger-height | The height of the trigger |
Arrow (optional)
An optional arrow element to render alongside a submenu. This can be used to help visually link the trigger item with ContextMenu.Content. Must be rendered inside ContextMenu.Content.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
width | number | 10 |
height | number | 5 |
@by/experience-system does not export ContextMenuArrow; ContextMenuSubTrigger includes a trailing chevron instead. For Radix’s optional arrow API, see the upstream docs.
ContextMenuItem
ContextMenuItem — the component that contains the context menu items.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
disabled | boolean | No default value |
onSelect | function | No default value |
textValue | string | No default value |
inset | boolean | No default value (experience system: aligns text with icon rows) |
color | enum | "neutral" ("neutral" | "error" — use error for irreversible actions) |
| Data attribute | Values |
|---|---|
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-color] | "neutral" | "error" |
ContextMenuGroup
ContextMenuGroup — used to group multiple items.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
ContextMenuLabel
ContextMenuLabel — used to render a label. It will not be focusable using arrow keys.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
inset | boolean | No default value (experience system) |
ContextMenuCheckboxItem
ContextMenuCheckboxItem — an item that can be controlled and rendered like a checkbox. Check rendering uses ContextMenuPrimitive.ItemIndicator internally with a Check icon.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
checked | boolean | 'indeterminate' | No default value |
onCheckedChange | function | No default value |
disabled | boolean | No default value |
onSelect | function | No default value |
textValue | string | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "checked" | "unchecked" | "indeterminate" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
ContextMenuRadioGroup
ContextMenuRadioGroup — used to group multiple ContextMenuRadioItem components.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
value | string | No default value |
onValueChange | function | No default value |
ContextMenuRadioItem
ContextMenuRadioItem — An item that can be controlled and rendered like a radio. The indicator is built in (ShapeBulletCircleFill).
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
value | string | No default value (required) |
disabled | boolean | No default value |
onSelect | function | No default value |
textValue | string | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "checked" | "unchecked" | "indeterminate" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
ItemIndicator
Renders when the parent CheckboxItem or RadioItem is checked. In @by/experience-system, indicators are internal to ContextMenuCheckboxItem and ContextMenuRadioItem (not a separate export).
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "checked" | "unchecked" | "indeterminate" |
ContextMenuSeparator
ContextMenuSeparator — used to visually separate items in the context menu.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
ContextMenuShortcut
ContextMenuShortcut — Design-system helper: a span for keyboard hints, aligned to the trailing edge when placed inside an item. Not a Radix primitive part.
ContextMenuSub
ContextMenuSub — contains all the parts of a submenu.
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | No default value |
open | boolean | No default value |
onOpenChange | function | No default value |
ContextMenuSubTrigger
ContextMenuSubTrigger — an item that opens a submenu. Must be rendered inside ContextMenuSub. Includes a trailing ArrowChevronRight icon.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
disabled | boolean | No default value |
textValue | string | No default value |
inset | boolean | No default value (experience system) |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
ContextMenuSubContent
ContextMenuSubContent — the component that pops out when a submenu is open. Must be rendered inside ContextMenuSub.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
loop | boolean | false |
onEscapeKeyDown | function | No default value |
onPointerDownOutside | function | No default value |
onFocusOutside | function | No default value |
onInteractOutside | function | No default value |
forceMount | boolean | No default value |
sideOffset | number | 0 |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionBoundary | Boundary | [] |
collisionPadding | number | Padding | 0 |
arrowPadding | number | 0 |
sticky | enum | "partial" |
hideWhenDetached | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-side] | "left" | "right" | "bottom" | "top" |
[data-align] | "start" | "end" | "center" |
| CSS variable | Description |
|---|---|
--radix-context-menu-content-transform-origin | The transform-origin computed from the content and arrow positions/offsets |
--radix-context-menu-content-available-width | The remaining width between the trigger and the boundary edge |
--radix-context-menu-content-available-height | The remaining height between the trigger and the boundary edge |
--radix-context-menu-trigger-width | The width of the trigger |
--radix-context-menu-trigger-height | The height of the trigger |
Accessibility
Uses roving tabindex to manage focus among menu items. Use ContextMenuLabel (optionally inset) to title sections when needed, and avoid relying only on shortcut text for essential information.
Keyboard interactions
| Key | Description |
|---|---|
Space | Activates the focused item. |
Enter | Activates the focused item. |
ArrowDown | Moves focus to the next item. |
ArrowUp | Moves focus to the previous item. |
ArrowRight / ArrowLeft | When focus is on ContextMenuSubTrigger, opens or closes the submenu depending on reading direction. |
Esc | Closes the context menu. |
For the full primitive reference and any upstream updates, see Radix UI — Context Menu.
Implementation: packages/experience-system/src/components/ContextMenu/ContextMenu.tsx. Codegen-oriented rules for AI agents: packages/experience-system/src/components/ContextMenu/ContextMenu_instructions.md.