ComponentsOverlays

Context Menu

A menu that opens where the pointer is. Right-click a target, or long-press it on a touch screen. Use it for shortcuts to actions that are also reachable another way, such as a More button or a keyboard shortcut.

Right-click this sample cardLong-press on touch · Shift + F10 with a keyboard

Favorite: off · View: List · Local sample only

No action selected.

Usage

Context menu usageTSX
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@tulipfarm/sprout/context-menu";

<ContextMenu>
  <ContextMenuTrigger tabIndex={0} role="group" aria-label="Sample card actions">
    Right-click this sample card
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuGroup>
      <ContextMenuLabel>Example actions</ContextMenuLabel>
      <ContextMenuItem onClick={open}>
        Open<ContextMenuShortcut>Enter</ContextMenuShortcut>
      </ContextMenuItem>
      <ContextMenuItem disabled>Unavailable</ContextMenuItem>
    </ContextMenuGroup>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive" onClick={remove}>
      Delete sample
    </ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

ContextMenuTrigger renders a div that wraps the target. Pass render to use another element. Give it tabIndex={0}, a role, and an aria-label when people should reach it with the keyboard.

Keyboard access

Right-click and long-press have no keyboard equivalent, so always offer the same actions another way. The items are plain Base UI menu parts, so the same elements work inside DropdownMenuContent. Open that menu with open and onOpenChange when the focused target receives Shift + F10 or the Menu key, and use finalFocus to return focus to the target.

Shared items with a keyboard fallbackTSX
const items = <>{/* ContextMenuItem, ContextMenuSub, … */}</>;

<ContextMenu>
  <ContextMenuTrigger
    tabIndex={0}
    role="group"
    aria-label="Sample card actions"
    onKeyDown={(event) => {
      if ((event.shiftKey && event.key === "F10") || event.key === "ContextMenu") {
        event.preventDefault();
        returnFocus.current = event.currentTarget;
        setOpen(true);
      }
    }}
  >
    Right-click this sample card
  </ContextMenuTrigger>
  <ContextMenuContent>{items}</ContextMenuContent>
</ContextMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
  <DropdownMenuTrigger render={<Button variant="outline" />}>Open sample actions</DropdownMenuTrigger>
  <DropdownMenuContent size="menu" finalFocus={() => returnFocus.current ?? true}>
    {items}
  </DropdownMenuContent>
</DropdownMenu>

Variants and states

ContextMenuItem variant is "default" or "destructive"; destructive items use the danger color and a tinted highlight. Items with a leading icon line up with labels and items that set inset. disabled items stay readable at half opacity and can still be reached with arrow keys. While a row syncs, disable the actions that would change it and keep the rest.

File rows

Right-click a row, or use its More button. While Contracts syncs, edits and trash are disabled but links still work.

  • Q3 budget.xlsxSpreadsheet · 48 KB
  • Field notes from the north greenhouse, week 14.pdfPDF · 2.1 MB
  • ContractsFolder · Syncing…

Checkboxes and radios

ContextMenuCheckboxItem and ContextMenuRadioItem show a check on the right. Checkboxes keep the menu open; pass closeOnClick to close after one choice. Disable the last visible column so the table never goes empty. ContextMenuSub nests a menu: ContextMenuSubTrigger adds a chevron and opens ContextMenuSubContent to the side.

Table header

Right-click the header to choose columns. Shift + F10 opens the same menu from the keyboard.

NameStatusOwnerDue date
Order seed traysTo doAma OwusuApr 12
Repair greenhouse ventIn progressKofi MensahApr 15

Row height: Comfortable · Sort: Due date

API

ContextMenu, ContextMenuTrigger, ContextMenuPortal, ContextMenuGroup, ContextMenuSub, ContextMenuRadioGroup, and ContextMenuSeparator wrap the Base UI context menu parts and take their props, including open, onOpenChange, and render.

ContextMenuContent renders its own portal and positioner. It opens beside the pointer and takes side ("right"), align ("start"), sideOffset (0),alignOffset (4), collisionPadding (8), container, and every Base UI popup prop. It is at least 9rem wide and grows with its items. ContextMenuSubContent takes the same props.

ContextMenuItem takes variant and inset. ContextMenuLabel, ContextMenuSubTrigger, and the checkbox and radio items take inset too. Put aContextMenuLabel inside a ContextMenuGroup; the group takes its name from the label. ContextMenuShortcut is a plainspan for hint text; it does not bind keys. Every part sets a data-slot.

Accessibility

The popup has role="menu". Arrow keys move, typing jumps to a matching item, Arrow Right opens a submenu and Arrow Left closes it, and Escape closes the menu. A long press opens it on touch screens, and the trigger turns off text selection and the system callout so the press isn’t interrupted. A focused trigger shows a focus ring.

Menus fade and scale in over 120ms. Motion turns off when reduced motion is requested. In forced-colors mode the highlighted item gets an outline.

Theme tokens

Menus use --sprout-surface, --sprout-line, --sprout-hover, --sprout-text, --sprout-muted, --sprout-danger, --sprout-danger-subtle, --sprout-focus, --sprout-shadow-overlay, and the panel and control radii. They need no brand color, so they look the same with the neutral and TulipFarm themes in light and dark mode.