# Context Menu

> A React context menu component: actions on right-click for grids and lists, and an iOS-style action sheet on a long press on touch screens.

- Type: React component (Overlays)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add context-menu`
- Docs: https://wingo-ui.com/components/context-menu

## Interaction

Right-click a tile and the menu grows out of the pointer on a quick spring. Hover or use the arrow keys to move the highlight, type a letter to jump, Right opens a submenu and Left closes it, Enter or Space picks, Escape closes. The shortcuts written next to items run them while the menu is open. Keyboard users reach the same menu from a focused tile with Shift+F10 or the Menu key. On a phone or a touch screen, press and hold a tile: it dips slightly while you hold, a haptic tick fires, and an action sheet slides up with the tile's name on top, 48px rows, submenus as pages with a Back row and a Cancel button at the thumb. Scrolling never opens it, and text selection with a mouse still works. With reduced motion the tile does not dip and the menu and sheet fade.

## Installation

```bash
npx wingo-ui@latest add context-menu
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/context-menu` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/context-menu.tsx`

npm dependencies: `npm i @radix-ui/react-context-menu motion`

Wingo UI dependencies (installed automatically):

- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Copy, ExternalLink, Heart, Trash2 } from "lucide-react"
import { ContextMenu, type ContextMenuEntry } from "@/components/ui/context-menu"

export function SavedListing({ favorite, onFavorite, onDelete }: { favorite: boolean; onFavorite: (next: boolean) => void; onDelete: () => void }) {
  const items: ContextMenuEntry[] = [
    { label: "Open", icon: <ExternalLink />, href: "/listings/park-slope-apartment" },
    { label: "Copy link", icon: <Copy />, shortcut: "mod+c", onSelect: () => navigator.clipboard.writeText("https://listings.example.com/a/123") },
    { type: "checkbox", label: "Favorite", icon: <Heart />, checked: favorite, onCheckedChange: onFavorite },
    { type: "separator" },
    { label: "Delete", icon: <Trash2 />, tone: "danger", onSelect: onDelete },
  ]

  return (
    <ContextMenu items={items} title="2-bedroom apartment, Park Slope" description="$489,000 · New York" cancelLabel="Cancel" backLabel="Back">
      {/* focusable, so Shift+F10 opens the menu from the keyboard */}
      <a href="/listings/park-slope-apartment" className="block rounded-2xl bg-surface p-3 shadow-raised">
        2-bedroom apartment, Park Slope
      </a>
    </ContextMenu>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` |  | The target. A single element (a card link, a row button) takes the handlers itself; anything else is wrapped in a <span>. Make it focusable so keyboard users can open the menu with Shift+F10. |
| `items?` | `MenuEntry[]` |  | The same entries as the dropdown menu: items ({ label, icon, description, shortcut, badge, onSelect, href, tone: "danger", keepOpen }), { type: "checkbox" }, { type: "radio" }, { type: "submenu", items }, { type: "group" }, { type: "label" } and { type: "separator" }. With no enabled entry the browser's own menu shows. |
| `title?` | `ReactNode` | `""` | The action sheet's centered header on phones, usually the name of what was pressed. Not shown on desktop. The demo uses each tile's title while this is empty. |
| `description?` | `ReactNode` | `""` | A quieter line under the sheet title. The demo shows each tile's price and city while this is empty. |
| `sheetTitle?` | `string` | `"Actions"` | The sheet's title for screen readers when there is no visible title. |
| `showCancel?` | `boolean` | `true` | The sheet's Cancel button at the bottom. The sheet still closes on a drag, a tap outside or Escape. |
| `cancelLabel?` | `string` | `"Cancel"` | The label of the sheet's Cancel button. |
| `backLabel?` | `string` | `"Back"` | The back row at the top of a submenu page in the sheet. |
| `size?` | `"sm" \| "md"` | `"md"` | Item height 32 / 36px with 13 / 14px text. 44px on touch either way; sheet rows are always 48px. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Panel corners 0 / 12 / 16 / 24px (full falls back to lg); items keep the panel radius minus its 6px padding. |
| `minWidth?` | `number` | `192` | The panel's minimum width in px. It grows for longer labels up to 384px, then labels truncate. |
| `pressFeedback?` | `boolean` | `true` | While a finger holds the target it dips to 0.97 on a snappy spring (iOS), and springs back when the sheet opens or the press is cancelled. Off under reduced motion. |
| `disabled?` | `boolean` | `false` | The browser's own menu shows instead, and a long press does nothing. |
| `responsive?` | `boolean` | `true` | Below 768px and on touch screens a long press (or a right-click) opens the action sheet. Off leaves Radix's own touch long press, which opens the floating menu at the finger. |
| `longPressDelay?` | `number` | `450` | How long a finger holds before the sheet opens, in ms (gestures.longPress). Moving more than 8px cancels, so scrolling never opens it. |
| `loop?` | `boolean` | `true` | Arrow keys wrap from the last item to the first (menu and sheet). |
| `modal?` | `boolean` | `true` | While the floating menu is open, the page behind ignores the pointer and does not scroll (Radix). The sheet is always modal. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the menu or the sheet opens or closes. |
| `container?` | `HTMLElement \| null` |  | Portal target for the menu and the sheet (default document.body). |
| `classNames?` | `Partial<Record<"content" \| "item" \| "icon" \| "label" \| "description" \| "shortcut" \| "separator" \| "sheet", string>>` |  | Extra classes for the parts: the panel (and sub panels), every row, row icons, labels, descriptions, shortcuts, separators and the sheet. |
| `className?` | `string` |  | Extra classes for the target; any other prop (aria-*, data-*, handlers) is passed to it too. It carries data-state (open / closed) and, in sheet mode, data-pressing while a long press is held. To keep the browser menu on a link inside the target, stop it there: onContextMenu={(e) => e.stopPropagation()}. |

## Parts

### ContextMenuRoot (component)

Compound API: decides the mode once (the floating Radix menu, or the action sheet on phones and touch screens) and every part follows it. There is no open prop: a context menu opens at a pointer. Props: { onOpenChange, modal, responsive, disabled, longPressDelay, pressFeedback, container, dir, children }.

### ContextMenuTrigger (component)

The target area (context-menu-trigger). One element child is used as the target (asChild by default); otherwise a <span>. Props: { asChild, disabled } & ComponentProps<"span">.

### ContextMenuContent (component)

The panel (context-menu-content) with the popover spring from the pointer, or the action sheet. JSX children (the parts below) are desktop-only Radix rows, so a content with children keeps the floating menu everywhere, as if responsive were false. Props: { items, size, radius, minWidth, loop, title, description, sheetTitle, showCancel, cancelLabel, backLabel, container, classNames } & Radix ContextMenu.Content props.

### ContextMenuItem (component)

A row. href renders an <a>; asChild takes a router Link and keeps the row layout. Its shortcut runs it while the menu is open. Props: { icon, description, shortcut, badge, tone: "neutral" | "danger", keepOpen, href, target, rel, asChild, disabled, onSelect }.

### ContextMenuCheckboxItem (component)

A row with a leading checkbox whose check draws itself; it stays open while toggling. Props: { checked, defaultChecked, onCheckedChange, icon, description, shortcut, keepOpen = true, disabled }.

### ContextMenuRadioGroup (component)

Holds one choice from a set of radio items. Props: { value, defaultValue, onValueChange }.

### ContextMenuRadioItem (component)

One choice in a radio group, marked with a trailing check. Props: { value, icon, description, keepOpen = true, disabled }.

### ContextMenuSub (component)

A nested menu: holds the open state of a side panel. Props: { open, defaultOpen, onOpenChange }.

### ContextMenuSubTrigger (component)

The row with a chevron that opens the nested menu. Props: { icon, description, badge, disabled }.

### ContextMenuSubContent (component)

The side panel of a nested menu. Props: { items, sideOffset, alignOffset, container }.

### ContextMenuGroup (component)

Groups related items into a section. Takes Radix Group props.

### ContextMenuLabel (component)

The small muted label of a section. Takes Radix Label props.

### ContextMenuSeparator (component)

A hairline between sections. Takes Radix Separator props.

### ContextMenuEntry (type)

One entry of items: an action, checkbox, radio group, submenu, label or separator, shared with the dropdown menu.

## Examples

- **File list with a selection**: Right-click inside a selection and the actions count it (“Delete 3 files”); outside it, only that row is selected.
- **Chat messages**: Reply, copy, forward and delete on each bubble; a long press on a phone opens the sheet with the message on top.
- **Press feedback on and off**: On touch the target dips to 0.97 while held, on a snappy spring; pressFeedback={false} keeps it still.
- **Disabled**: disabled hands the right-click back to the browser's own menu.
- **Parts with custom JSX**: ContextMenuRoot, Trigger and Content with JSX rows. Custom parts are desktop-only: this menu stays a floating panel on phones.

## See also

- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

---

Canonical: https://wingo-ui.com/components/context-menu
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
