Skip to the content

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.

Right-click here, or focus and press Shift + F10
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.

Right-click here, or focus and press Shift + F10
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.

Right-click here, or focus and press Shift + F10
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.

Right-click here, or focus and press Shift + F10
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

Keyboard
KeyBehaviour
ShiftF10On 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.
HomeorEndMoves to the first or last item.
A–ZType-ahead: moves to the next item whose text starts with the letters typed.
EnterorSpaceChooses 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).
EscapeCloses the menu and returns focus to the trigger area.

API

ContextMenu

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

ContextMenu props
PropTypeDefaultDescription
dir"ltr" | "rtl"The reading direction. The provider's dir by default.
modalbooleanWhether 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.
openboolean

ContextMenuTrigger

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

ContextMenuTrigger props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.
disabledbooleanLets the browser's own context menu open instead.

ContextMenuContent

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

ContextMenuContent props
PropTypeDefaultDescription
alignOffsetnumberDistance in pixels along the pointer's side.
arrowPaddingnumber
asChildboolean
avoidCollisionsboolean
collisionBoundaryBoundary | Boundary[]
collisionPaddingnumber | Partial<Record<"top" | "bottom" | "left" | "right", number>>
forceMounttrueUsed to force mounting when more control is needed. Useful when controlling animation with React animation libraries.
hideWhenDetachedboolean
loopbooleanWhether 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.

ContextMenuItem props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.
disabledbooleanSkips the item and ignores clicks.
insetbooleanIndent 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.
textValuestringThe text type-ahead matches, when the content is not plain text.
variant"default" | "destructive"defaultdefault, or destructive for an action that removes something.

ContextMenuCheckboxItem

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

ContextMenuCheckboxItem props
PropTypeDefaultDescription
asChildboolean
checkedboolean | "indeterminate"Whether it is checked: true, false or "indeterminate". Pair it with onCheckedChange.
disabledbooleanSkips the item and ignores clicks.
onCheckedChange((checked: boolean) => void)Called with the new state when it is chosen.
onSelect((event: Event) => void)
textValuestring

ContextMenuRadioItem

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

ContextMenuRadioItem props
PropTypeDefaultDescription
valuerequiredstringThe value this item stands for.
asChildboolean
disabledbooleanSkips the item and ignores clicks.
onSelect((event: Event) => void)
textValuestring

ContextMenuLabel

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

ContextMenuLabel props
PropTypeDefaultDescription
asChildboolean
insetbooleanIndent the label to line up with items that have a check or dot.

ContextMenuSeparator

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

ContextMenuSeparator props
PropTypeDefaultDescription
asChildboolean

ContextMenuShortcut

Renders a span and passes it every other prop.

ContextMenuGroup

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

ContextMenuGroup props
PropTypeDefaultDescription
asChildboolean

ContextMenuPortal

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

ContextMenuPortal props
PropTypeDefaultDescription
containerElement | DocumentFragment | nullSpecify a container element to portal the content into.
forceMounttrueUsed 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.

ContextMenuSub props
PropTypeDefaultDescription
defaultOpenboolean
onOpenChange((open: boolean) => void)Called with true or false when it opens or closes.
openbooleanWhether the submenu is open, when you control it.

ContextMenuSubContent

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

ContextMenuSubContent props
PropTypeDefaultDescription
align"start" | "end"Controls the direction the subcontent appears from its anchor menu item Default: start
alignOffsetnumber
arrowPaddingnumber
asChildboolean
avoidCollisionsboolean
collisionBoundaryBoundary | Boundary[]
collisionPaddingnumber | Partial<Record<"top" | "bottom" | "left" | "right", number>>
forceMounttrueUsed to force mounting when more control is needed. Useful when controlling animation with React animation libraries.
hideWhenDetachedboolean
loopbooleanWhether keyboard navigation should loop around
onEscapeKeyDown((event: KeyboardEvent) => void)
onFocusOutside((event: FocusOutsideEvent) => void)
onInteractOutside((event: FocusOutsideEvent | PointerDownOutsideEvent) => void)
onPointerDownOutside((event: PointerDownOutsideEvent) => void)
sideOffsetnumberDistance in pixels from its trigger.
sticky"always" | "partial"
updatePositionStrategy"always" | "optimized"

ContextMenuSubTrigger

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

ContextMenuSubTrigger props
PropTypeDefaultDescription
asChildboolean
disabledbooleanSkips the item and ignores clicks.
insetbooleanIndent the item to line up with items that have a check or dot.
textValuestring

ContextMenuRadioGroup

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

ContextMenuRadioGroup props
PropTypeDefaultDescription
valuerequiredstringThe chosen item's value. Pair it with onValueChange.
asChildboolean
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.

Theme tokens
TokenUsed for
--accentbackground
--accent-foregroundtext
--borderbackground
--control-hovertext
--destructive-strongtext
--destructive-subtlebackground
--muted-foregroundtext
--popoverbackground
--popover-foregroundtext