Siteplane UI

Context Menu

Show actions for an element on right-click or long press.

Markdown
Right-click or long-press here
"use client";

import { CopyIcon, PencilIcon, ShareIcon, Trash2Icon } from "lucide-react";
import {
  ContextMenu,
  ContextMenuItem,
  ContextMenuPopup,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/siteplane/components/context-menu";

export default function Example() {
  return (
    <ContextMenu>
      <ContextMenuTrigger
        className="flex h-28 w-full max-w-sm items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm"
        tabIndex={0}
      >
        Right-click or long-press here
      </ContextMenuTrigger>
      <ContextMenuPopup>
        <ContextMenuItem>
          <PencilIcon aria-hidden="true" />
          Edit
          <ContextMenuShortcut>⌘E</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          <CopyIcon aria-hidden="true" />
          Copy
          <ContextMenuShortcut>⌘C</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          <ShareIcon aria-hidden="true" />
          Share
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <Trash2Icon aria-hidden="true" />
          Delete
          <ContextMenuShortcut>⌘⌫</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuPopup>
    </ContextMenu>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/context-menu

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/card

Usage

import { PencilIcon } from "lucide-react";
import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuPopup,
  ContextMenuItem,
} from "@/siteplane/components/context-menu";

<ContextMenu>
  <ContextMenuTrigger
    className="flex h-28 w-full items-center justify-center rounded-lg border border-dashed"
    tabIndex={0}
  >
    Right-click this area
  </ContextMenuTrigger>
  <ContextMenuPopup>
    <ContextMenuItem>
      <PencilIcon />
      Edit
    </ContextMenuItem>
  </ContextMenuPopup>
</ContextMenu>;

Composition

ContextMenu
├── ContextMenuTrigger
└── ContextMenuPopup
    ├── ContextMenuGroup → ContextMenuGroupLabel + items
    ├── ContextMenuItem / CheckboxItem / RadioGroup
    ├── ContextMenuSeparator
    └── ContextMenuSub → SubTrigger + ContextMenuSubPopup

Groups and submenus

Label related actions with ContextMenuGroup and ContextMenuGroupLabel, and move secondary actions into a submenu.

Right-click or long-press here
"use client";

import {
  CopyIcon,
  MailIcon,
  PencilIcon,
  ShareIcon,
  Trash2Icon,
} from "lucide-react";
import {
  ContextMenu,
  ContextMenuGroup,
  ContextMenuGroupLabel,
  ContextMenuItem,
  ContextMenuPopup,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubPopup,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/siteplane/components/context-menu";

export default function Example() {
  return (
    <ContextMenu>
      <ContextMenuTrigger
        className="flex h-28 w-full max-w-sm items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm"
        tabIndex={0}
      >
        Right-click or long-press here
      </ContextMenuTrigger>
      <ContextMenuPopup>
        <ContextMenuGroup>
          <ContextMenuGroupLabel>File</ContextMenuGroupLabel>
          <ContextMenuItem>
            <PencilIcon />
            Rename
          </ContextMenuItem>
          <ContextMenuItem>
            <CopyIcon />
            Duplicate
            <ContextMenuShortcut>⌘D</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuSub>
            <ContextMenuSubTrigger>
              <ShareIcon />
              Share
            </ContextMenuSubTrigger>
            <ContextMenuSubPopup>
              <ContextMenuItem>
                <MailIcon />
                Email link
              </ContextMenuItem>
              <ContextMenuItem>
                <CopyIcon />
                Copy link
              </ContextMenuItem>
            </ContextMenuSubPopup>
          </ContextMenuSub>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <Trash2Icon />
          Move to trash
        </ContextMenuItem>
      </ContextMenuPopup>
    </ContextMenu>
  );
}

Checkbox and radio items

Use checkbox items for view toggles and a radio group for a single choice, each under a group label. variant="switch" only changes the indicator. The check draws in and out like the Checkbox check and appears instantly under reduced motion. A radio item shows its check in the leading slot; for a row that leads with a logo or an avatar, indicatorPosition="end" moves the check to the end of the row, as on Menu.

Right-click or long-press here
"use client";

import { useState } from "react";
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuGroup,
  ContextMenuGroupLabel,
  ContextMenuPopup,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/siteplane/components/context-menu";

export default function Example() {
  const [showHidden, setShowHidden] = useState(false);
  const [sortBy, setSortBy] = useState("name");
  return (
    <ContextMenu>
      <ContextMenuTrigger
        className="flex h-28 w-full max-w-sm items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm"
        tabIndex={0}
      >
        Right-click or long-press here
      </ContextMenuTrigger>
      <ContextMenuPopup>
        <ContextMenuGroup>
          <ContextMenuGroupLabel>View</ContextMenuGroupLabel>
          <ContextMenuCheckboxItem
            checked={showHidden}
            onCheckedChange={setShowHidden}
          >
            Show hidden files
          </ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem defaultChecked>
            Show file extensions
          </ContextMenuCheckboxItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuGroupLabel>Sort by</ContextMenuGroupLabel>
          <ContextMenuRadioGroup value={sortBy} onValueChange={setSortBy}>
            <ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
            <ContextMenuRadioItem value="modified">
              Date modified
            </ContextMenuRadioItem>
            <ContextMenuRadioItem value="size">Size</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuCheckboxItem variant="switch" defaultChecked>
          Compact view
        </ContextMenuCheckboxItem>
      </ContextMenuPopup>
    </ContextMenu>
  );
}

Use ContextMenuLinkItem for navigation; it renders a real link and closes the menu after activation.

Right-click or long-press here
"use client";

import { ExternalLinkIcon, SettingsIcon } from "lucide-react";
import {
  ContextMenu,
  ContextMenuLinkItem,
  ContextMenuPopup,
  ContextMenuTrigger,
} from "@/siteplane/components/context-menu";

export default function Example() {
  return (
    <ContextMenu>
      <ContextMenuTrigger
        className="flex h-28 w-full max-w-sm items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm"
        tabIndex={0}
      >
        Right-click or long-press here
      </ContextMenuTrigger>
      <ContextMenuPopup>
        <ContextMenuLinkItem href="#project">
          <ExternalLinkIcon />
          Open project
        </ContextMenuLinkItem>
        <ContextMenuLinkItem href="#project-settings">
          <SettingsIcon />
          Project settings
        </ContextMenuLinkItem>
      </ContextMenuPopup>
    </ContextMenu>
  );
}

Glass

Set variant="glass" on ContextMenuPopup when the menu opens over a photo, a video or a color surface, and on a ContextMenuSubPopup so its submenu matches. Rows, icons and their states stay the same. Glass shows every component with a glass variant.

The gradient only makes the glass visible. It is not part of the component.

Right-click or long-press here
"use client";

import { CopyIcon, PencilIcon, ShareIcon, Trash2Icon } from "lucide-react";
import { Card } from "@/siteplane/components/card";
import {
  ContextMenu,
  ContextMenuItem,
  ContextMenuPopup,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/siteplane/components/context-menu";

export default function Example() {
  return (
    <ContextMenu>
      <ContextMenuTrigger
        className="h-28 w-full max-w-sm items-center justify-center text-sm"
        render={<Card variant="glass" />}
        tabIndex={0}
      >
        Right-click or long-press here
      </ContextMenuTrigger>
      <ContextMenuPopup variant="glass">
        <ContextMenuItem>
          <PencilIcon aria-hidden="true" />
          Edit
        </ContextMenuItem>
        <ContextMenuItem>
          <CopyIcon aria-hidden="true" />
          Copy
        </ContextMenuItem>
        <ContextMenuItem>
          <ShareIcon aria-hidden="true" />
          Share
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <Trash2Icon aria-hidden="true" />
          Delete
        </ContextMenuItem>
      </ContextMenuPopup>
    </ContextMenu>
  );
}

API reference

ContextMenu, ContextMenuPortal and the group, radio and submenu parts follow Base UI Context Menu. Popup includes its Portal, Positioner and a scrolling viewport; do not add another Portal around it. LinkItem renders a native link. Shortcut renders kbd and does not register a shortcut. SubPopup opens on the inline-end side. ContextMenuPrimitive exports the Base UI namespace. Styled className props are strings.

Component properties
PropTypeDefaultDescription
ContextMenuPopup.sidePositioner sidebottomPreferred edge.
ContextMenuPopup.alignstart | center | endstartAlignment.
ContextMenuPopup.sideOffsetPositioner offset0Gap.
ContextMenuPopup.alignOffsetPositioner offsetunsetAlignment offset.
ContextMenuPopup.widthauto | trigger | content | at-least-triggerautotrigger matches the width of the ContextMenuTrigger area; at-least-trigger is at least that wide. With a custom anchor, both use the anchor's width. Every width but trigger is at most 20rem, or the trigger area's width for at-least-trigger when it is wider, and stays within the available space; a longer row label ends in an ellipsis and a group label wraps.
ContextMenuPopup.anchorPositioner anchorpointer positionCustom anchor.
ContextMenuPopup.portalPropsPortal.PropsunsetPortal options.
ContextMenuPopup.variantdefault | glassdefaultglass shows a tinted, blurred view of what lies behind the popup.
ContextMenuItem.variantdefault | destructivedefaultAction tone. A destructive item colors its label and its icon red.
Item / GroupLabel / SubTrigger.insetbooleanunset (off)Reserve the icon slot so the label lines up with icon rows.
ContextMenuCheckboxItem.variantdefault | switchdefaultCheck or switch indicator; retains menuitemcheckbox semantics.
ContextMenuRadioItem.indicatorPositionstart | endstartWhere the check shows: in the leading slot, or at the end of an identity row that leads with a logo or an avatar.
ContextMenuCheckboxItem.sizesm | md | lg | xllgSwitch size. Menu rows stay compact at every control size, so the switch does not follow the provider.
ContextMenuSubPopup.align / sideOffsetalignment / offsetstart / 0Submenu positioning.
ContextMenuSubPopup.alignOffsetoffset-5 unless align=centerSubmenu alignment correction.
ContextMenuSubPopup.variantdefault | glassdefaultSet glass to match a glass parent menu.
open / defaultOpenbooleanfalseVisibility.
onOpenChange(open, eventDetails) => voidunsetVisibility callback.
Item.onClickMouseEventHandlerunsetApplication action.
CheckboxItem.checked / onCheckedChangeboolean / callbackuncontrolled / unsetControlled checked state.
RadioGroup.value / onValueChangestring / callbackuncontrolled / unsetControlled choice.
ContextMenuLinkItem.closeOnClickbooleantrueCloses the menu after activating a link.
ContextMenuLinkItem.variant / insettone / booleandefault / unsetLink-row presentation.

Modal, disabled, focus, dismissal and close-on-click behavior follow Base UI. A disabled item cannot be activated; the submenu trigger supplies the chevron. SubPopup accepts width plus the listed offsets, not arbitrary portal or anchor options.

Read the installed source.

Usage notes

Start every action item with a fitting lucide icon, such as PencilIcon for Edit or Trash2Icon for Delete; checkbox and radio items show their own indicator instead. Provide the same actions through a visible button menu for users who do not discover right-click. Context Menu is not a replacement for all primary actions. Arrow keys move through the items, including disabled ones, which cannot be activated. Enter activates an item, and Escape closes the menu with focus restoration. Use real links for navigation. Selection here is menu state, not native form submission.