bx

Context Menu

Displays a menu located at the pointer, triggered by a right click.

Right-click here
tsx
<ContextMenu>
  <ContextMenuTrigger for="row">
    <div className="flex h-32 w-72 items-center justify-center rounded-md border border-dashed text-sm text-muted-foreground">
      Right-click here
    </div>
  </ContextMenuTrigger>
  <ContextMenuContent id="row" ariaLabel="Actions">
    <ContextMenuLabel>Actions</ContextMenuLabel>
    <ContextMenuSeparator />
    <ContextMenuItem>Cut <ContextMenuShortcut>⌘X</ContextMenuShortcut></ContextMenuItem>
    <ContextMenuItem>Copy <ContextMenuShortcut>⌘C</ContextMenuShortcut></ContextMenuItem>
    <ContextMenuItem>Paste <ContextMenuShortcut>⌘V</ContextMenuShortcut></ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

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

Terminal
$ bext ui add context-menu

Usage

tsx
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut } from "@bext-stack/ui/primitives"

<ContextMenu>
  <ContextMenuTrigger for="row-ctx">
    <div className="p-8 border rounded-md">Right-click here</div>
  </ContextMenuTrigger>
  <ContextMenuContent id="row-ctx">
    <ContextMenuItem>
      Cut <ContextMenuShortcut>⌘X</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuItem>Copy <ContextMenuShortcut>⌘C</ContextMenuShortcut></ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

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

Right-click, or focus and press Shift+F10
tsx
<ContextMenu>
  <ContextMenuTrigger for="view">
    <div className="flex h-32 w-72 items-center justify-center rounded-md border border-dashed">Right-click, or focus and press Shift+F10</div>
  </ContextMenuTrigger>
  <ContextMenuContent id="view" ariaLabel="View options">
    <ContextMenuCheckboxItem checked>Show toolbar</ContextMenuCheckboxItem>
    <ContextMenuCheckboxItem>Show sidebar</ContextMenuCheckboxItem>
    <ContextMenuSeparator />
    <ContextMenuRadioGroup>
      <ContextMenuRadioItem checked>Compact</ContextMenuRadioItem>
      <ContextMenuRadioItem>Comfortable</ContextMenuRadioItem>
    </ContextMenuRadioGroup>
    <ContextMenuSeparator />
    <ContextMenuSub>
      <ContextMenuSubTrigger inset>More tools</ContextMenuSubTrigger>
      <ContextMenuSubContent>
        <ContextMenuItem>Save page as</ContextMenuItem>
        <ContextMenuItem>Developer tools</ContextMenuItem>
      </ContextMenuSubContent>
    </ContextMenuSub>
    <ContextMenuItem inset disabled>Reload</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.