Context Menu
Displays a menu located at the pointer, triggered by a right click.
ContextMenu is a right-click menu (the native desktop pattern). The trigger wraps an active area; right-clicking inside it opens the menu at the pointer position. The area is also a tab stop: with focus on it, Shift+F10 or the Menu key opens the same menu.
Composition is the same as DropdownMenu: Items, Label, Separator, Shortcut. Submenus are available (Sub / SubTrigger / SubContent) for hierarchical navigation.
For regular "kebab menu" actions (visible button), use DropdownMenu instead. ContextMenu is a hidden interaction, reserved for experienced users.
Installation
$ bext ui add context-menu
Copy and paste the following into src/components/ui/context-menu.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="context-menu" hooks.
Usage
Anatomy
<ContextMenu>
├── <ContextMenuTrigger for="ctx-id">
│ └── <div>Active area (right-click here)</div>
└── <ContextMenuContent id="ctx-id">
├── <ContextMenuLabel>Actions</ContextMenuLabel>
├── <ContextMenuItem>Cut <ContextMenuShortcut>⌘X</ContextMenuShortcut></ContextMenuItem>
├── <ContextMenuSeparator />
└── <ContextMenuItem>Delete</ContextMenuItem>API Reference
ContextMenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| for | string | — | Matches the id of the Content. |
| focusable | boolean | true | Makes the area a tab stop (tabindex="0") so the menu can be opened from the keyboard. Pass false when the child is already focusable. |
ContextMenuContent
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Matches Trigger.for. Fixed-positioned at the pointer position on right-click. |
| ariaLabel | string | — | Accessible name (aria-label) of the menu. |
ContextMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "destructive" | "default" | "destructive" applies text-destructive + focus:bg-destructive/10. |
| inset | boolean | — | pl-8 to align with items that have an indicator (checkbox/radio). |
| href | string | — | If set, renders <a>. Otherwise a <div> that only closes the menu. |
| disabled | boolean | — | Sets data-disabled and aria-disabled="true": no pointer events, 50% opacity, skipped by the arrow keys. |
ContextMenuCheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | — | Initial state. Check mark shown on the left when true; activating the item toggles it. |
ContextMenuRadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Groups ContextMenuRadioItem elements (role="group"): activating one unchecks the others. |
ContextMenuRadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | — | Initial state. Radio dot shown when true. |
ContextMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
| inset | boolean | — | pl-8 for alignment. |
ContextMenuShortcut
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Shown on the right, muted (e.g. "⌘X"). |
ContextMenuSub / SubTrigger / SubContent
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Submenu: SubContent is revealed when the pointer hovers the Sub wrapper, or with ArrowRight / Enter on the SubTrigger. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="ctxmenu-trigger" | Trigger | data-target="#id" | The island listens for the contextmenu event on these elements. |
| data-state | Trigger | "open" | "closed" | Set by the island while the menu is open. |
| data-ui="close" | Item | — | Closes on click. |
| data-inset | Item / Label | present | Reflects the inset prop (pl-8). |
| data-variant | Item | "default" | "destructive" | Reflects the variant prop. |
| data-disabled | Item, CheckboxItem, RadioItem | present when disabled | Removes pointer events and dims the item. |
| data-state | CheckboxItem, RadioItem | "checked" | "unchecked" | Starts from the checked prop. The island updates it, with aria-checked and the indicator, when the item is activated. |
| data-overlay data-light | Content | hidden by default | Light overlay: the next click anywhere closes it. |
Keyboard
| Key | Action |
|---|---|
| Shift+F10 / Menu key | With focus on the trigger area: opens the menu on the area and focuses the first item. |
| ArrowDown / ArrowUp | Moves to the next or previous item, wrapping at the ends. Disabled items are skipped. |
| Home / End | Moves to the first or the last item. |
| Enter / Space | Activates the focused item and closes the menu. Toggles a checkbox item, selects a radio item, opens a submenu. |
| ArrowRight / ArrowLeft | On a SubTrigger, ArrowRight opens the submenu and focuses its first item. In a submenu, ArrowLeft closes it and returns to the SubTrigger. |
| A letter | Moves to the next item whose label starts with that letter. |
| Escape | Closes the submenu if one has focus, otherwise the menu. Focus returns to the trigger area. |
| Tab | Closes the menu. |
Accessibility
The contextmenu event is a desktop pattern that is not discoverable on mobile or for keyboard-only users. Always pair it with a DropdownMenu or a visible action that offers the same functionality.
ContextMenuContent has role="menu"; items have role="menuitem", menuitemcheckbox or menuitemradio (with aria-checked) and tabindex="-1". ContextMenuSeparator sets role="separator", ContextMenuRadioGroup sets role="group", and ContextMenuLabel is plain text that is never focused. Give the menu a name with the ariaLabel prop: unlike a dropdown, it has no button to take its name from.
The trigger area has tabindex="0" and aria-haspopup="menu". It carries data-state rather than aria-expanded, because aria-expanded is not valid on an element without a widget role. The area has no role of its own, so a screen reader announces its content only: say in visible text that a menu is available.
Focus: opened from the keyboard, the menu focuses its first item. Opened with a right-click, the menu itself takes focus so the arrow keys work straight away. Escape, Tab or activating an item closes it and returns focus to the trigger area.
Examples
Checkbox, radio and submenu
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page