# Context Menu

A menu of actions that opens at the pointer on right click or long press.

Rows take `variant` for their look — `neutral` (the default) and `danger` for a destructive action.

## Installation

```tsx
import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
} from "@makeplane/propel/components/context-menu";
```
## Usage

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import { Icon } from "@makeplane/propel/components/icon";
import { Shortcut } from "@makeplane/propel/components/shortcut";
import { ClipboardOutline, CopyOutline, UndoOutline, DeleteOutline } from "@makeplane/propel/icons";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

export default function BasicDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem
          icon={<Icon icon={UndoOutline} />}
          aria-keyshortcuts="Meta+Z"
          trailing={<Shortcut keys="⌘Z" />}
          label="Undo"
        />
        <ContextMenuItem
          icon={<Icon icon={CopyOutline} />}
          aria-keyshortcuts="Meta+C"
          trailing={<Shortcut keys="⌘C" />}
          label="Copy"
        />
        <ContextMenuItem
          icon={<Icon icon={ClipboardOutline} />}
          aria-keyshortcuts="Meta+V"
          trailing={<Shortcut keys="⌘V" />}
          label="Paste"
        />
        <ContextMenuSeparator />
        <ContextMenuItem variant="danger" icon={<Icon icon={DeleteOutline} />} label="Delete" />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```
## Examples

### Submenu

Nest a `ContextMenuSubmenu` to reveal more destinations beside the parent menu.
Root and submenu both size themselves `auto`: as wide as their rows, between the 204px floor and
the 400px ceiling. The rule is on the [Sizing](/foundations/sizing#popup-widths) page.

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSubmenu,
  ContextMenuSubmenuContent,
  ContextMenuSubmenuTrigger,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import { Icon } from "@makeplane/propel/components/icon";
import {
  CopyOutline,
  FolderOpenOutline,
  EditOutline,
  DeleteOutline,
} from "@makeplane/propel/icons";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

export default function SubmenuDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem icon={<Icon icon={EditOutline} />} label="Rename" />
        <ContextMenuItem icon={<Icon icon={CopyOutline} />} label="Duplicate" />
        <ContextMenuSubmenu>
          <ContextMenuSubmenuTrigger icon={<Icon icon={FolderOpenOutline} />} label="Move to" />
          <ContextMenuSubmenuContent>
            <ContextMenuItem label="Mobile app" />
            <ContextMenuItem label="Web app" />
            <ContextMenuItem label="Design system" />
          </ContextMenuSubmenuContent>
        </ContextMenuSubmenu>
        <ContextMenuSeparator />
        <ContextMenuItem variant="danger" icon={<Icon icon={DeleteOutline} />} label="Delete" />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

### Link items

`ContextMenuLinkItem` renders a real anchor, so a row can navigate while keeping menu-item behavior.
Pass `external` for outbound links (`target="_blank"`, `rel="noreferrer noopener"`, trailing arrow).

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuLinkItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import { Icon } from "@makeplane/propel/components/icon";
import { Shortcut } from "@makeplane/propel/components/shortcut";
import { CopyOutline, NewTabArrowOutline, LinkOutline } from "@makeplane/propel/icons";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

export default function LinkItemsDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuLinkItem
          icon={<Icon icon={NewTabArrowOutline} />}
          href="#work-item"
          external
          label="Open in new tab"
        />
        <ContextMenuLinkItem
          icon={<Icon icon={LinkOutline} />}
          href="#activity"
          label="Go to activity"
        />
        <ContextMenuSeparator />
        <ContextMenuItem
          icon={<Icon icon={CopyOutline} />}
          aria-keyshortcuts="Meta+C"
          trailing={<Shortcut keys="⌘C" />}
          label="Copy link"
        />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

### Selection rows

`ContextMenuCheckboxItem` toggles independently; `ContextMenuRadioItem` rows wrapped in a
`ContextMenuRadioGroup` share one `value`. Both pass `closeOnClick={false}` so the menu stays open
while you adjust the options. The radio group passes `marker="radio"` because this popup also holds a
checkbox row: next to a box, the default trailing tick would make the two kinds of choice read the
same. A single-select group on its own keeps the default tick and no leading gutter.

```tsx
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import * as React from "react";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

export default function SelectionRowsDemo() {
  const [wrap, setWrap] = React.useState(true);
  const [sort, setSort] = React.useState("manual");

  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click for view options
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuCheckboxItem
          checked={wrap}
          onCheckedChange={setWrap}
          closeOnClick={false}
          label="Wrap titles"
        />
        <ContextMenuSeparator />
        {/* This popup also holds a checkbox row, so the group opts into the ring: a box on one line
            and a trailing tick on the others would read as two different kinds of choice. */}
        <ContextMenuRadioGroup value={sort} onValueChange={setSort} marker="radio">
          <ContextMenuRadioItem value="manual" closeOnClick={false} label="Manual order" />
          <ContextMenuRadioItem value="updated" closeOnClick={false} label="Last updated" />
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

### Single select

A `selected` row shows a trailing check at the row's inline-end; forwarding `closeOnClick={false}` moves the check without dismissing the menu. Pass `selected` on every row of a single-select list so unselected rows keep the trailing gutter and the labels hold a common width.

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import * as React from "react";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

const SORT_OPTIONS = [
  { key: "manual", label: "Manual" },
  { key: "created", label: "Created date" },
  { key: "due", label: "Due date" },
  { key: "priority", label: "Priority" },
] as const;

export default function SelectedDemo() {
  const [sortBy, setSortBy] = React.useState("manual");
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click for sort options
      </ContextMenuTrigger>
      <ContextMenuContent>
        {SORT_OPTIONS.map((option) => (
          <ContextMenuItem
            key={option.key}
            selected={sortBy === option.key}
            closeOnClick={false}
            onClick={() => setSortBy(option.key)}
            label={option.label}
          />
        ))}
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

### With description

A `description` gives each row a muted second line; passing it switches the row to a taller, top-aligned layout, so the icon and check line up with the label.

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import { Icon } from "@makeplane/propel/components/icon";
import { GlobeOutline, LockOutline } from "@makeplane/propel/icons";
import * as React from "react";

const triggerClass =
  "flex h-32 w-72 items-center justify-center rounded-lg border-sm border-dashed border-subtle text-body-xs-regular text-tertiary select-none";

export default function WithDescriptionDemo() {
  const [visibility, setVisibility] = React.useState("private");
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click to change visibility
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem
          icon={<Icon icon={LockOutline} tint="secondary" />}
          description="Accessible only by invite"
          selected={visibility === "private"}
          closeOnClick={false}
          onClick={() => setVisibility("private")}
          label="Private"
        />
        <ContextMenuItem
          icon={<Icon icon={GlobeOutline} tint="secondary" />}
          description="Anyone in the workspace except Guests can join"
          selected={visibility === "public"}
          closeOnClick={false}
          onClick={() => setVisibility("public")}
          label="Public"
        />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```
## API Reference

### ContextMenu

Root of the context menu: holds the open state and the pointer position the menu anchors to, and
renders no element of its own. Wrap a `ContextMenuTrigger` and a `ContextMenuContent` in it; the
menu opens on right click or long press anywhere inside the trigger.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Whether the menu is open. Controlled; pair with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the menu is open on mount. Uncontrolled. |
| `onOpenChange` | `((open: boolean, eventDetails: ContextMenuRootChangeEventDetails) => void)` | — | Called with the next open state when the menu opens or closes. |
| `children` | `ReactNode` | — | The trigger and menu surface (`ContextMenuTrigger`, `ContextMenuContent`). |

### ContextMenuTrigger

The behavior surface that opens the menu on right click or long press — Base UI applies no
styling of its own, so give it a look by grafting it onto your own element via `render`:

```tsx
<ContextMenuTrigger render={<MyCanvasArea />}>Right-click here</ContextMenuTrigger>;
```

Maps 1:1 to `ContextMenu.Trigger`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, ContextMenuTriggerState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### ContextMenuContent

The root context-menu surface: Base UI's portal and positioner wrapped around an elevated,
scrollable `OverlayPanel` holding the flat `role="menu"` popup. Anchors to the pointer and sizes
itself like a dropdown. Rows (`ContextMenuItem`, `ContextMenuSeparator`, …) are its children. A
submenu's own surface is `ContextMenuSubmenuContent`, not this part.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-end" \| "inline-start"` | — | Which side of the anchor the popup opens toward. Leave it unset at the root: an explicit `side` opts out of Base UI's pointer-hugging offsets, so the menu detaches from the cursor. A submenu opens toward `inline-end` unless told otherwise. |
| `sideOffset` | `number \| OffsetFunction` | — | Distance in px between the anchor and the popup. |
| `align` | `"center" \| "start" \| "end"` | — | Alignment of the popup relative to the anchor along `side`. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset in px along the align axis — slides the popup along the anchor's edge without changing `align`. |
| `collisionPadding` | `Padding` | `16` | Minimum gap in px between the popup and the viewport edge it would otherwise touch. |
| `collisionBoundary` | `Boundary` | — | The element or rect the popup is confined to when avoiding collisions. Base UI's clipping ancestors when unset. |
| `collisionAvoidance` | `CollisionAvoidance` | — | What to do when the popup would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset. |
| `sticky` | `boolean` | `false` | Keep the popup on screen after its anchor scrolls out of view, instead of following it out. |
| `positionMethod` | `"absolute" \| "fixed"` | `"absolute"` | Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors. |
| `anchor` | `Element \| VirtualElement \| RefObject<Element \| null> \| (() => Element \| VirtualElement \| null) \| null` | — | Position against this element instead of the pointer — an element, a ref, a virtual element, or a function returning one. Geometry only; right-click targeting is unchanged. |
| `disableAnchorTracking` | `boolean` | `false` | Stop tracking layout shifts of the anchor — position once, on open. |
| `sizing` | `"menu" \| "anchor" \| "auto" \| "sm" \| "md" \| "lg"` | `auto` | How the menu popup is sized — the same rule as `Menu`. `auto` is the content's width between the 204px floor and 400px ceiling, held while open; `anchor` matches the anchor's width instead. The deprecated `menu`/`sm`/`md`/`lg` aliases render as `auto`. The default applies to an omitted _and_ an explicitly `undefined` value. |

### ContextMenuItem

The ready-made menu row: grafts Base UI's `Item` behavior onto the styled `ContextMenuItem` and
composes its region parts — an optional leading icon, the label and an optional description, the
single-select check, and optional trailing content.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={Copy} />`. |
| `description` | `string` | — | Muted secondary line under the label. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |
| `selected` | `boolean` | — | Single-select selected state. Pass it on **every** `ContextMenuItem` row of a single-select list (`true` or `false`) so each row reserves the trailing check gutter and its labels stay a common width; omit it entirely on plain action rows, which then render no gutter. `ContextMenuLinkItem` and `ContextMenuSubmenuTrigger` have no `selected` prop or gutter of their own — mixing either into the same list as selected rows gives their labels more room than the checked ones. |

### ContextMenuCheckboxItem

The ready-made toggleable context-menu row: grafts Base UI's `CheckboxItem` behavior onto the
styled row and bakes a kept-mounted tick indicator, so `checked` / `defaultChecked` /
`onCheckedChange` forward straight to Base UI and the tick reads state from context.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action. |
| `icon` | `ReactNode` | — | Leading element after the tick, e.g. `<Icon icon={Columns} />`. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |

### ContextMenuRadioGroup

Wraps `ContextMenuRadioItem`s sharing one `value` — Base UI's radio-group state, no chrome.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The content of the component. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, MenuRadioGroupState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `marker` | `"radio" \| "check"` | `check` | How the group marks its current row. Defaults to `check` — the trailing tick every other single-select row uses. Pass `radio` only when this group shares a popup with a multi-select (checkbox) group and the two need telling apart. |

### ContextMenuRadioItem

The ready-made single-select context-menu row: grafts Base UI's `RadioItem` behavior onto the
styled row and marks the current row the way its `ContextMenuRadioGroup`'s `marker` says. `check`
(the default): a kept-mounted trailing tick on the selected row. `radio`: a leading ring drawn on
every row with only its inner dot hiding, for a popup that also holds a checkbox group. Wrap rows
in a `ContextMenuRadioGroup` carrying `value`/`onValueChange`; both the selected state and the
marker come from context.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action. |
| `icon` | `ReactNode` | — | Leading element, e.g. `<Icon icon={AlignLeft} />`. With the group's `marker="radio"` it sits after the ring; with the default tick it leads the row. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |

### ContextMenuLinkItem

The ready-made navigational menu row: grafts Base UI's `LinkItem` behavior onto the styled
`ContextMenuLinkItem` and composes its region parts — a leading icon, the label, and optional
trailing content.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={ExternalLink} />`. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |
| `external` | `boolean` | `false` | The row leaves the app. Sets the three things a link out has to carry, together: a trailing arrow so the row says where it goes, `target="_blank"`, and `rel="noreferrer noopener"`. They are one prop because they are one decision, and because two of them are easy to forget — a missing `rel` is a security default, not a style nit. Both are overridable: pass your own `target` or `rel` and it wins. Passing `trailing` also wins, in which case the row is yours to annotate. |

### ContextMenuGroup

Groups related rows; provide `aria-label`/`aria-labelledby` when the group needs a name.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The content of the component. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, MenuGroupState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### ContextMenuSeparator

Ready-made divider between groups of items: Base UI's `ContextMenu.Separator` behavior (the
`separator` role) grafted onto the styled thin rule.

No component-specific props — accepts the standard HTML attributes for the element it renders.

### ContextMenuSubmenu

Groups all parts of a nested submenu — its `ContextMenuSubmenuTrigger` row and the
`ContextMenuSubmenuContent` that opens beside it. Holds the submenu's own open state and renders
no element of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Whether the submenu is open. Controlled; pair with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the submenu is open on mount. Uncontrolled. |
| `onOpenChange` | `((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)` | — | Called with the next open state when the submenu opens or closes. |
| `children` | `ReactNode` | — | The submenu's trigger row and the `ContextMenuContent` it opens. |

### ContextMenuSubmenuTrigger

The ready-made submenu trigger: grafts Base UI's `SubmenuTrigger` behavior onto the styled
`ContextMenuSubmenuTrigger` and composes its region parts — a leading icon, the label, optional
trailing content, and the caret that points toward the submenu.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={Folder} />`. |
| `trailing` | `ReactNode` | — | Content pinned before the submenu caret, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |
