Propel

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.

Right-click here
Show code
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 { ClipboardPaste, Copy, Scissors, Trash2 } from "lucide-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 BasicDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem
          icon={<Icon icon={Scissors} />}
          aria-keyshortcuts="Meta+X"
          trailing={<Shortcut keys="⌘X" />}
          label="Cut"
        />
        <ContextMenuItem
          icon={<Icon icon={Copy} />}
          aria-keyshortcuts="Meta+C"
          trailing={<Shortcut keys="⌘C" />}
          label="Copy"
        />
        <ContextMenuItem
          icon={<Icon icon={ClipboardPaste} />}
          aria-keyshortcuts="Meta+V"
          trailing={<Shortcut keys="⌘V" />}
          label="Paste"
        />
        <ContextMenuSeparator />
        <ContextMenuItem variant="danger" icon={<Icon icon={Trash2} />} label="Delete" />
      </ContextMenuContent>
    </ContextMenu>
  );
}

Installation

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

Usage

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 { ClipboardPaste, Copy, Scissors, Trash2 } from "lucide-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 BasicDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem
          icon={<Icon icon={Scissors} />}
          aria-keyshortcuts="Meta+X"
          trailing={<Shortcut keys="⌘X" />}
          label="Cut"
        />
        <ContextMenuItem
          icon={<Icon icon={Copy} />}
          aria-keyshortcuts="Meta+C"
          trailing={<Shortcut keys="⌘C" />}
          label="Copy"
        />
        <ContextMenuItem
          icon={<Icon icon={ClipboardPaste} />}
          aria-keyshortcuts="Meta+V"
          trailing={<Shortcut keys="⌘V" />}
          label="Paste"
        />
        <ContextMenuSeparator />
        <ContextMenuItem variant="danger" icon={<Icon icon={Trash2} />} label="Delete" />
      </ContextMenuContent>
    </ContextMenu>
  );
}

Examples

Nest a ContextMenuSubmenu to reveal more destinations beside the parent menu.

Right-click here
Show code
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSubmenu,
  ContextMenuSubmenuContent,
  ContextMenuSubmenuTrigger,
  ContextMenuTrigger,
} from "@makeplane/propel/components/context-menu";
import { Icon } from "@makeplane/propel/components/icon";
import { Copy, FolderInput, PencilLine, Trash2 } from "lucide-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 SubmenuDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem icon={<Icon icon={PencilLine} />} label="Rename" />
        <ContextMenuItem icon={<Icon icon={Copy} />} label="Duplicate" />
        <ContextMenuSubmenu>
          <ContextMenuSubmenuTrigger icon={<Icon icon={FolderInput} />} 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={Trash2} />} label="Delete" />
      </ContextMenuContent>
    </ContextMenu>
  );
}

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).

Right-click here
Show code
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 { Copy, ExternalLink, Link2 } from "lucide-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 LinkItemsDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger render={<div className={triggerClass} />}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuLinkItem
          icon={<Icon icon={ExternalLink} />}
          href="#work-item"
          external
          label="Open in new tab"
        />
        <ContextMenuLinkItem icon={<Icon icon={Link2} />} href="#activity" label="Go to activity" />
        <ContextMenuSeparator />
        <ContextMenuItem
          icon={<Icon icon={Copy} />}
          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.

Right-click for view options
Show code
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 />
        <ContextMenuRadioGroup value={sort} onValueChange={setSort}>
          <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 leading check; 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 leading gutter.

Right-click for sort options
Show code
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>
  );
}

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.

PropTypeDefaultDescription
openbooleanWhether the menu is open. Controlled; pair with `onOpenChange`.
defaultOpenbooleanfalseWhether 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.
childrenReactNodeThe 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`.

PropTypeDefaultDescription
renderReactElement<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.

PropTypeDefaultDescription
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.
sideOffsetnumber | OffsetFunctionDistance in px between the anchor and the popup.
align"center" | "start" | "end"Alignment of the popup relative to the anchor along `side`.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the popup along the anchor's edge without changing `align`.
collisionPaddingPadding5Minimum gap in px between the popup and the viewport edge it would otherwise touch.
collisionBoundaryBoundaryThe element or rect the popup is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat 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.
stickybooleanfalseKeep 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.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullPosition 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.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
sizing"menu" | "anchor" | "auto" | "sm" | "md" | "lg"menuHow the menu popup is sized — width for every value, plus a height cap for `menu`. `menu` is the dropdown's own 192–320px width range with a 384px height cap, `anchor` matches the trigger's width, `auto` hugs the content with a small floor, and `sm`/`md`/`lg` are the fixed picker widths (256/288/384px). Unlike `ContextMenuSubmenuContent`, this surface always resolves to a sizing — 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 — a leading single-select check, an optional leading icon, the label, and optional trailing content.

PropTypeDefaultDescription
label(required)stringPrimary row label.
variant"neutral" | "danger""neutral"Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action.
iconReactNodeLeading element before the label, e.g. `<Icon icon={Copy} />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`.
selectedbooleanSingle-select selected state. Pass it on **every** `ContextMenuItem` row of a single-select list (`true` or `false`) so each row reserves the leading check gutter and stays aligned; 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 leaves their labels unaligned with 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.

PropTypeDefaultDescription
label(required)stringPrimary row label.
variant"neutral" | "danger""neutral"Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action.
iconReactNodeLeading element after the tick, e.g. `<Icon icon={Columns} />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>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.

PropTypeDefaultDescription
childrenReactNodeThe content of the component.
renderReactElement<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.

ContextMenuRadioItem

The ready-made single-select context-menu row: grafts Base UI's `RadioItem` behavior onto the styled row and bakes a kept-mounted radio dot. Wrap rows in a `ContextMenuRadioGroup` carrying `value`/`onValueChange`; the dot reads the selected state from context.

PropTypeDefaultDescription
label(required)stringPrimary row label.
variant"neutral" | "danger""neutral"Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action.
iconReactNodeLeading element after the radio dot, e.g. `<Icon icon={AlignLeft} />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>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.

PropTypeDefaultDescription
label(required)stringPrimary row label.
variant"neutral" | "danger""neutral"Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action.
iconReactNodeLeading element before the label, e.g. `<Icon icon={ExternalLink} />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`.
externalbooleanfalseThe 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.

PropTypeDefaultDescription
childrenReactNodeThe content of the component.
renderReactElement<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.

PropTypeDefaultDescription
openbooleanWhether the submenu is open. Controlled; pair with `onOpenChange`.
defaultOpenbooleanfalseWhether 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.
childrenReactNodeThe 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.

PropTypeDefaultDescription
label(required)stringPrimary row label.
variant"neutral" | "danger""neutral"Row look. `neutral` is the standard text hierarchy and `danger` marks a destructive action.
iconReactNodeLeading element before the label, e.g. `<Icon icon={Folder} />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>Content pinned before the submenu caret, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`.