ContextMenu

A right-click triggered contextual menu built on Radix UI ContextMenu. Supports sub-menus, checkbox items, radio groups, separators, keyboard shortcuts, and full keyboard navigation.

Installation

npm install @radix-ui/react-context-menu

Usage

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuPortal,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuRadioGroup,
} from "@/components/ui/context-menu";

Examples

Basic Right-Click Menu

Wrap any element in ContextMenuTrigger to make it the activation target. ContextMenuContent appears at the cursor position on right-click (or long-press on touch devices).

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
} from "@/components/ui/context-menu";
 
export function BasicContextMenu() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground select-none">
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-48">
        <ContextMenuItem>Open</ContextMenuItem>
        <ContextMenuItem>Rename</ContextMenuItem>
        <ContextMenuItem>Duplicate</ContextMenuItem>
        <ContextMenuItem className="text-destructive">Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  );
}

With Keyboard Shortcuts

ContextMenuShortcut renders a right-aligned hint showing the keyboard shortcut for an item. It is presentational only — wire up the actual shortcut handling with a global key listener in your app.

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuShortcut,
} from "@/components/ui/context-menu";
 
export function ContextMenuWithShortcuts() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground select-none">
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-52">
        <ContextMenuItem>
          Cut
          <ContextMenuShortcut>⌘X</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          Copy
          <ContextMenuShortcut>⌘C</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          Paste
          <ContextMenuShortcut>⌘V</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          Select All
          <ContextMenuShortcut>⌘A</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  );
}

With Sub-Menu

Use ContextMenuSub, ContextMenuSubTrigger, and ContextMenuSubContent to nest a secondary menu inside any item. The sub-menu opens on hover or arrow-key navigation.

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
  ContextMenuSeparator,
} from "@/components/ui/context-menu";
 
export function ContextMenuWithSub() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground select-none">
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-52">
        <ContextMenuItem>Open</ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger>Share</ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-40">
            <ContextMenuItem>Email link</ContextMenuItem>
            <ContextMenuItem>Copy link</ContextMenuItem>
            <ContextMenuSeparator />
            <ContextMenuItem>Twitter / X</ContextMenuItem>
            <ContextMenuItem>LinkedIn</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuItem className="text-destructive">Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  );
}

With Checkbox and Radio Items

ContextMenuCheckboxItem renders a checkmark when selected. ContextMenuRadioGroup with ContextMenuRadioItem implements mutually exclusive selection. Both are controlled via state.

import { useState } from "react";
import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuCheckboxItem,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuLabel,
  ContextMenuSeparator,
} from "@/components/ui/context-menu";
 
export function ContextMenuWithSelections() {
  const [showGrid, setShowGrid] = useState(true);
  const [showRulers, setShowRulers] = useState(false);
  const [zoom, setZoom] = useState("100");
 
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground select-none">
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-52">
        <ContextMenuLabel>View options</ContextMenuLabel>
        <ContextMenuCheckboxItem
          checked={showGrid}
          onCheckedChange={setShowGrid}
        >
          Show grid
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem
          checked={showRulers}
          onCheckedChange={setShowRulers}
        >
          Show rulers
        </ContextMenuCheckboxItem>
        <ContextMenuSeparator />
        <ContextMenuLabel>Zoom level</ContextMenuLabel>
        <ContextMenuRadioGroup value={zoom} onValueChange={setZoom}>
          <ContextMenuRadioItem value="50">50%</ContextMenuRadioItem>
          <ContextMenuRadioItem value="100">100%</ContextMenuRadioItem>
          <ContextMenuRadioItem value="150">150%</ContextMenuRadioItem>
          <ContextMenuRadioItem value="200">200%</ContextMenuRadioItem>
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  );
}

With Separators and Labels

Use ContextMenuLabel to add non-interactive section headings and ContextMenuSeparator to draw visual dividers between groups of related items.

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
} from "@/components/ui/context-menu";
 
export function ContextMenuWithGroups() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground select-none">
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-56">
        <ContextMenuGroup>
          <ContextMenuLabel>File</ContextMenuLabel>
          <ContextMenuItem>
            New file
            <ContextMenuShortcut>⌘N</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Open…
            <ContextMenuShortcut>⌘O</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuLabel>Edit</ContextMenuLabel>
          <ContextMenuItem>
            Undo
            <ContextMenuShortcut>⌘Z</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Redo
            <ContextMenuShortcut>⇧⌘Z</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuItem className="text-destructive">
          Move to trash
          <ContextMenuShortcut>⌫</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  );
}

API Reference

ContextMenu

Root component. Manages open state and provides context to all children.

PropTypeDefaultDescription
childrenReactNodeMust contain ContextMenuTrigger and ContextMenuContent.

ContextMenuTrigger

PropTypeDefaultDescription
asChildbooleanfalseMerges props onto the child element instead of a wrapping <span>.
classNamestringAdditional CSS classes.

ContextMenuContent

PropTypeDefaultDescription
classNamestringAdditional CSS classes applied to the menu panel.
sideOffsetnumber4Distance in px from the trigger point.
align"start" | "center" | "end""center"Alignment relative to the trigger.

ContextMenuItem

PropTypeDefaultDescription
insetbooleanfalseAdds left padding to align with items that have icons.
classNamestringAdditional CSS classes.
onSelect(event: Event) => voidCalled when the item is selected.
disabledbooleanfalsePrevents selection and applies disabled styling.

ContextMenuCheckboxItem

PropTypeDefaultDescription
checkedboolean | "indeterminate"Controlled checked state.
onCheckedChange(checked: boolean) => voidCalled when the checked state changes.
disabledbooleanfalsePrevents interaction.
classNamestringAdditional CSS classes.

ContextMenuRadioGroup

PropTypeDefaultDescription
valuestringThe currently selected radio value.
onValueChange(value: string) => voidCalled when the selected value changes.

ContextMenuRadioItem

PropTypeDefaultDescription
valuestringThe value this item represents. Required.
disabledbooleanfalsePrevents selection.
classNamestringAdditional CSS classes.

ContextMenuLabel

PropTypeDefaultDescription
insetbooleanfalseAdds left padding to align with inset menu items.
classNamestringAdditional CSS classes.

ContextMenuSeparator

PropTypeDefaultDescription
classNamestringAdditional CSS classes.

ContextMenuShortcut

Presentational <span> for displaying keyboard shortcut hints. Accepts all standard <span> HTML attributes.

ContextMenuSub / ContextMenuSubTrigger / ContextMenuSubContent

Compose these three primitives to build nested menus. ContextMenuSubTrigger accepts inset: boolean. ContextMenuSubContent accepts the same props as ContextMenuContent.

ContextMenuPortal

Renders its children in a portal appended to document.body. Wraps ContextMenuContent automatically.

ContextMenuGroup

Semantic grouping wrapper. Accepts all standard <div> HTML attributes.

Accessibility

  • The context menu follows the ARIA Menu pattern. The trigger has aria-haspopup="menu" and aria-expanded managed by Radix.
  • The menu is opened by the browser's native context menu gesture (right-click, Shift+F10, or the Menu key). Radix intercepts and replaces this event.
  • Full keyboard navigation is built in: ArrowUp / ArrowDown move between items, ArrowRight opens a sub-menu, ArrowLeft closes it, Enter / Space selects an item, and Escape closes the menu.
  • ContextMenuLabel renders as a non-interactive element (role="none"); screen readers read its text as group context.
  • ContextMenuShortcut is hidden from the accessibility tree (aria-hidden). The shortcut is a visual affordance only — ensure every action is reachable without it.
  • Items disabled with the disabled prop receive aria-disabled="true" and are skipped during keyboard navigation.
  • The menu is rendered inside a ContextMenuPortal to avoid z-index and overflow clipping issues.

Live View

Open ContextMenu in Storybook →