ComponentsMenu
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"Usage
A context menu is a shortcut: it repeats actions that are also reachable another way, such as a row's dropdown menu. Nothing should be available only here, since a context menu cannot be discovered by looking at the page.
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'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.
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.
Checkbox and radio items
Show or hide columns with checkbox items, and choose one density with radio items.
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.
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.
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 aremenuitem,menuitemcheckbox(witharia-checked) ormenuitemradio. A submenu trigger hasaria-haspopup="menu"andaria-expanded. The trigger area keeps its own role: Radix adds none. - Labels
- Items are named by their text.
ContextMenuLabelnames 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 |
|---|---|
| ShiftF10 | 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. |
| HomeorEnd | Moves to the first or last item. |
| A–Z | Type-ahead: moves to the next item whose text starts with the letters typed. |
| EnterorSpace | 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 |
|---|---|---|---|
valuerequired | 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 |
|---|---|---|---|
valuerequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| 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 |