# Context menu

A menu of actions for an area or a row, opened with a right-click, a long press or the keyboard's menu key.

- **Import:** `import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup } from "@booleanpress/ui/context-menu"`
- **Radix Context Menu:** <https://www.radix-ui.com/primitives/docs/components/context-menu>
- **APG Menu:** <https://www.w3.org/WAI/ARIA/apg/patterns/menubar/>
- **Page:** <https://ui.booleanpress.com/components/context-menu> · @booleanpress/ui 0.2.0

## Usage

A context menu is a shortcut: it repeats actions that are also reachable another way, such as a row's [dropdown menu](/components/dropdown-menu). Nothing should be available only here, since a context menu cannot be discovered by looking at the page.

```tsx
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@booleanpress/ui/context-menu"

export function LogEntry() {
  return (
    <ContextMenu>
      <ContextMenuTrigger tabIndex={0}>Delivery to ops@example.com</ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem onSelect={() => resend()}>Resend</ContextMenuItem>
        <ContextMenuItem variant="destructive" onSelect={() => remove()}>Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

`ContextMenuTrigger` is the area that answers the right-click; the menu opens where the pointer is. Give the area `tabIndex={0}` (or make it a focusable element) so keyboard users can focus it and open the menu with Shift + F10 or the menu key. The items are the [dropdown menu](/components/dropdown-menu)'s: `ContextMenuCheckboxItem`, `ContextMenuRadioItem` inside `ContextMenuRadioGroup`, `ContextMenuSub` for a nested list, `ContextMenuLabel`, `ContextMenuSeparator`, `ContextMenuGroup` and `ContextMenuShortcut`, which shows a key hint without binding the key. `variant="destructive"` colours an item that removes something.

## Examples

### Basic

A right-click on the dashed area opens a menu of actions at the pointer.

```tsx
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@booleanpress/ui/context-menu"

export default function ContextMenuBasic() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-md items-center justify-center rounded-md border border-dashed px-4 text-center text-sm/normal text-muted-foreground outline-none focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid" tabIndex={0}>
        Right-click here, or focus and press Shift + F10
      </ContextMenuTrigger>
      <ContextMenuContent className="w-48">
        <ContextMenuItem>View details</ContextMenuItem>
        <ContextMenuItem>Resend</ContextMenuItem>
        <ContextMenuItem>Copy message ID</ContextMenuItem>
        <ContextMenuItem>Add a note</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Submenus

`ContextMenuSub` opens a second list with → (← in right-to-left pages), with icons in the rows.

```tsx
import { ArchiveIcon, DownloadIcon, ExternalLinkIcon, FolderIcon, InboxIcon, PrinterIcon, RotateCwIcon } from "lucide-react"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@booleanpress/ui/context-menu"

export default function ContextMenuSubmenus() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-md items-center justify-center rounded-md border border-dashed px-4 text-center text-sm/normal text-muted-foreground outline-none focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid" tabIndex={0}>
        Right-click here, or focus and press Shift + F10
      </ContextMenuTrigger>
      <ContextMenuContent className="w-48">
        <ContextMenuItem><ExternalLinkIcon />Open</ContextMenuItem>
        <ContextMenuItem><RotateCwIcon />Resend</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuSub>
          <ContextMenuSubTrigger><DownloadIcon />Export as</ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-36">
            <ContextMenuItem>CSV</ContextMenuItem>
            <ContextMenuItem>JSON</ContextMenuItem>
            <ContextMenuItem>EML file</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSub>
          <ContextMenuSubTrigger><FolderIcon />Move to</ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-36">
            <ContextMenuItem><InboxIcon />Inbox</ContextMenuItem>
            <ContextMenuItem><ArchiveIcon />Archive</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuItem><PrinterIcon />Print</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Checkbox and radio items

Show or hide columns with checkbox items, and choose one density with radio items.

```tsx
import { useState } from "react"
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@booleanpress/ui/context-menu"

export default function ContextMenuCheckboxRadio() {
  const [columns, setColumns] = useState({ status: true, recipient: true, opens: false })
  const [density, setDensity] = useState("comfortable")

  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-md items-center justify-center rounded-md border border-dashed px-4 text-center text-sm/normal text-muted-foreground outline-none focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid" tabIndex={0}>
        Right-click here, or focus and press Shift + F10
      </ContextMenuTrigger>
      <ContextMenuContent className="w-52">
        <ContextMenuLabel>Columns</ContextMenuLabel>
        <ContextMenuCheckboxItem checked={columns.status} onCheckedChange={(status) => setColumns({ ...columns, status })}>
          Status
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem checked={columns.recipient} onCheckedChange={(recipient) => setColumns({ ...columns, recipient })}>
          Recipient
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem checked={columns.opens} onCheckedChange={(opens) => setColumns({ ...columns, opens })}>
          Opens
        </ContextMenuCheckboxItem>
        <ContextMenuSeparator />
        <ContextMenuLabel>Density</ContextMenuLabel>
        <ContextMenuRadioGroup value={density} onValueChange={setDensity}>
          <ContextMenuRadioItem value="comfortable">Comfortable</ContextMenuRadioItem>
          <ContextMenuRadioItem value="compact">Compact</ContextMenuRadioItem>
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Destructive item

A key's actions on its row, with `variant="destructive"` on the one that revokes it.

```tsx
import { CopyIcon, KeyRoundIcon, PencilIcon, Trash2Icon } from "lucide-react"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@booleanpress/ui/context-menu"

export default function ContextMenuDestructive() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex w-full max-w-md items-center gap-3 rounded-md border bg-card px-4 py-3 text-sm/normal outline-none focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid" tabIndex={0}>
        <KeyRoundIcon aria-hidden="true" className="size-3.5 text-muted-foreground" />
        <span className="font-medium text-foreground">Production key</span>
        <span className="ms-auto font-mono text-xs text-muted-foreground">bp_live_…8f2a</span>
      </ContextMenuTrigger>
      <ContextMenuContent className="w-48">
        <ContextMenuItem><PencilIcon />Rename</ContextMenuItem>
        <ContextMenuItem><CopyIcon />Copy key ID</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive"><Trash2Icon />Revoke key</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Shortcuts

`ContextMenuShortcut` shows each action's key at the end of its row.

```tsx
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger } from "@booleanpress/ui/context-menu"

export default function ContextMenuShortcuts() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-md items-center justify-center rounded-md border border-dashed px-4 text-center text-sm/normal text-muted-foreground outline-none focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid" tabIndex={0}>
        Right-click here, or focus and press Shift + F10
      </ContextMenuTrigger>
      <ContextMenuContent className="w-52">
        <ContextMenuItem>Reply<ContextMenuShortcut>R</ContextMenuShortcut></ContextMenuItem>
        <ContextMenuItem>Forward<ContextMenuShortcut>F</ContextMenuShortcut></ContextMenuItem>
        <ContextMenuItem>Copy link<ContextMenuShortcut>⌘L</ContextMenuShortcut></ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem>Mark as read<ContextMenuShortcut>⇧U</ContextMenuShortcut></ContextMenuItem>
        <ContextMenuItem variant="destructive">Delete<ContextMenuShortcut>⌫</ContextMenuShortcut></ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Accessibility

**Semantics.** The content is `role="menu"`; items are `menuitem`, `menuitemcheckbox` (with `aria-checked`) or `menuitemradio`. A submenu trigger has `aria-haspopup="menu"` and `aria-expanded`. The trigger area keeps its own role: Radix adds none.

**Labels.** Items are named by their text. `ContextMenuLabel` names a section visually. The trigger area needs visible text or a name that says what it is.

**Focus.** Opening moves focus into the menu; opened from the keyboard, the arrow keys then reach the items. Closing returns focus to the trigger area. The menu is modal: the page behind it cannot be reached.

**Known limits.**

- A context menu is hidden until it is opened. Repeat its actions somewhere visible.
- The trigger area must be focusable for keyboard users; Radix does not make it so.
- Only text and icons in items; a menu is not a place for form controls.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Shift + F10 | On the focused trigger area (or with the menu key), opens the menu. |
| ↓ | Moves to the next item; it stops at the last, or wraps to the first with `loop`. |
| ↑ | Moves to the previous item; it stops at the first, or wraps to the last with `loop`. |
| Home or End | Moves to the first or last item. |
| A–Z | Type-ahead: moves to the next item whose text starts with the letters typed. |
| Enter or Space | Chooses the item and closes the menu; on a checkbox or radio item, also changes it. |
| → | On a submenu trigger, opens its submenu (← in right-to-left pages). |
| ← | In a submenu, closes it and returns to its trigger (→ in right-to-left pages). |
| Escape | Closes the menu and returns focus to the trigger area. |

## API

### ContextMenu

Renders Radix ContextMenu.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `dir` | `"ltr" \| "rtl"` |  | The reading direction. The provider's `dir` by default. |
| `modal` | `boolean` |  | Whether the rest of the page is blocked while it is open. `true` by default. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  |  |

### ContextMenuTrigger

Renders Radix ContextMenu.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `disabled` | `boolean` |  | Lets the browser's own context menu open instead. |

### ContextMenuContent

Renders Radix ContextMenu.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alignOffset` | `number` |  | Distance in pixels along the pointer's side. |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"top" \| "bottom" \| "left" \| "right", number>>` |  |  |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `loop` | `boolean` |  | Whether keyboard navigation should loop around |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Called when Escape is pressed. Call `event.preventDefault()` to keep it open. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  |  |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  |  |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  |  |
| `sticky` | `"always" \| "partial"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### ContextMenuItem

Renders Radix ContextMenu.Item and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `inset` | `boolean` |  | Indent the item to line up with items that have a check or dot. |
| `onSelect` | `((event: Event) => void)` |  | Called when the item is chosen. Call `event.preventDefault()` to keep the menu open. |
| `textValue` | `string` |  | The text type-ahead matches, when the content is not plain text. |
| `variant` | `"default" \| "destructive"` | `default` | `default`, or `destructive` for an action that removes something. |

### ContextMenuCheckboxItem

Renders Radix ContextMenu.CheckboxItem and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `checked` | `boolean \| "indeterminate"` |  | Whether it is checked: `true`, `false` or `"indeterminate"`. Pair it with `onCheckedChange`. |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `onCheckedChange` | `((checked: boolean) => void)` |  | Called with the new state when it is chosen. |
| `onSelect` | `((event: Event) => void)` |  |  |
| `textValue` | `string` |  |  |

### ContextMenuRadioItem

Renders Radix ContextMenu.RadioItem and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The value this item stands for. |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `onSelect` | `((event: Event) => void)` |  |  |
| `textValue` | `string` |  |  |

### ContextMenuLabel

Renders Radix ContextMenu.Label and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `inset` | `boolean` |  | Indent the label to line up with items that have a check or dot. |

### ContextMenuSeparator

Renders Radix ContextMenu.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### ContextMenuShortcut

Renders a `span` and passes it every other prop.

### ContextMenuGroup

Renders Radix ContextMenu.Group and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### ContextMenuPortal

Renders Radix ContextMenu.Portal and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `Element \| DocumentFragment \| null` |  | Specify a container element to portal the content into. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### ContextMenuSub

Renders Radix ContextMenu.Sub and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether the submenu is open, when you control it. |

### ContextMenuSubContent

Renders Radix ContextMenu.SubContent and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "end"` |  | Controls the direction the subcontent appears from its anchor menu item Default: start |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"top" \| "bottom" \| "left" \| "right", number>>` |  |  |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `loop` | `boolean` |  | Whether keyboard navigation should loop around |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  |  |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  |  |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  |  |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  |  |
| `sideOffset` | `number` |  | Distance in pixels from its trigger. |
| `sticky` | `"always" \| "partial"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### ContextMenuSubTrigger

Renders Radix ContextMenu.SubTrigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `inset` | `boolean` |  | Indent the item to line up with items that have a check or dot. |
| `textValue` | `string` |  |  |

### ContextMenuRadioGroup

Renders Radix ContextMenu.RadioGroup and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The chosen item's value. Pair it with `onValueChange`. |
| `asChild` | `boolean` |  |  |
| `onValueChange` | `((value: string) => void)` |  | Called with the value of the item that was chosen. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="context-menu"` (ContextMenu), `data-slot="context-menu-trigger"` (ContextMenuTrigger), `data-slot="context-menu-content"` (ContextMenuContent), `data-slot="context-menu-item"` (ContextMenuItem), `data-slot="context-menu-checkbox-item"` (ContextMenuCheckboxItem), `data-slot="context-menu-radio-item"` (ContextMenuRadioItem), `data-slot="context-menu-label"` (ContextMenuLabel), `data-slot="context-menu-separator"` (ContextMenuSeparator), `data-slot="context-menu-shortcut"` (ContextMenuShortcut), `data-slot="context-menu-group"` (ContextMenuGroup), `data-slot="context-menu-portal"` (ContextMenuPortal), `data-slot="context-menu-sub"` (ContextMenuSub), `data-slot="context-menu-sub-content"` (ContextMenuSubContent), `data-slot="context-menu-sub-trigger"` (ContextMenuSubTrigger), `data-slot="context-menu-radio-group"` (ContextMenuRadioGroup), and `data-inset`, `data-variant`.

## Theming

The menu is a floating surface: `--popover` and `--popover-foreground`. The highlighted item uses `--accent`; icons, checks and dots use `--control-hover`, and `--muted-foreground` on the highlighted item; labels use `--muted-foreground`; a destructive item uses `--destructive-strong`, on `--destructive-subtle` when highlighted.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--border` | background |
| `--control-hover` | text |
| `--destructive-strong` | text |
| `--destructive-subtle` | background |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
