Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Context Menu

A menu of actions revealed by right-clicking (or long-pressing) a target — items, submenus, separators, labels, shortcuts, and checkbox/radio selections.

Status
stable
Since
0.1.0
Accessibility pattern
APG menu

Last updated

Right-click here

Install

Add Context Menu from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/context-menu

The same command installs the registry items it composes: @vegastack/floating-surface.

Usage

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

<ContextMenu>
  <ContextMenuTrigger>Right-click here</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Copy</ContextMenuItem>
    <ContextMenuItem tone="destructive">Delete</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>;

Anatomy

ContextMenu is a compound component built on Base UI's ContextMenu. Unlike DropdownMenu, the trigger is a right-click target area (not a button); the content portals to <body> and positions itself against the pointer where the menu was opened.

ContextMenu
ContextMenuCheckboxItem
ContextMenuContent
ContextMenuGroup
ContextMenuItem
ContextMenuLabel
ContextMenuRadioGroup
ContextMenuRadioItem
ContextMenuSeparator
ContextMenuShortcut
ContextMenuSub
ContextMenuSubContent
ContextMenuSubTrigger
ContextMenuTrigger — data-slot="context-menu-trigger"
<ContextMenu>
  <ContextMenuTrigger />
  <ContextMenuContent>
    <ContextMenuGroup>
      <ContextMenuLabel />
      <ContextMenuItem />
      <ContextMenuShortcut />
    </ContextMenuGroup>

    <ContextMenuSeparator />

    <ContextMenuCheckboxItem />

    <ContextMenuRadioGroup>
      <ContextMenuRadioItem />
    </ContextMenuRadioGroup>

    <ContextMenuSub>
      <ContextMenuSubTrigger />
      <ContextMenuSubContent>
        <ContextMenuItem />
      </ContextMenuSubContent>
    </ContextMenuSub>
  </ContextMenuContent>
</ContextMenu>
  • ContextMenu — the root that groups every part (ContextMenu.Root; renders no element).
  • ContextMenuTrigger — the area you right-click, long-press, or focus and open with Menu / Shift+F10 (data-slot="context-menu-trigger"; renders a <div>). Use render to compose with your own element.
  • ContextMenuContent — the floating popup; portals to <body> and positions against the pointer (data-slot="context-menu-content"). Accepts side, align, sideOffset, collisionPadding, and exposes portalProps / positionerProps.
  • ContextMenuItem — a selectable action (data-slot="context-menu-item"). Supports tone="destructive" and inset.
  • ContextMenuGroup + ContextMenuLabel — a labelled section; the label becomes the group's accessible name.
  • ContextMenuSeparator — a divider between sections (role="separator").
  • ContextMenuShortcut — an inline-end-aligned keyboard hint inside an item.
  • ContextMenuCheckboxItem — a togglable item with a check indicator (checked / onCheckedChange).
  • ContextMenuRadioGroup + ContextMenuRadioItem — single-select items (value / onValueChange).
  • ContextMenuSub + ContextMenuSubTrigger + ContextMenuSubContent — a nested submenu.

Examples

A rich menu with grouped items, labels, keyboard shortcuts, checkbox + radio selections, a disabled item, a destructive action, and a nested submenu.

Right-click here

Items

Items take leading lucide-react icons as children, a trailing ContextMenuShortcut, and a tone="destructive" for delete/remove actions. Use inset to align items that have no leading icon with those that do.

<ContextMenuItem>
  <Copy />
  Copy
  <ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem tone="destructive">
  <Trash2 />
  Delete
</ContextMenuItem>

Inset items

Set inset on ContextMenuItem, ContextMenuLabel, and ContextMenuSubTrigger to add left padding so icon-less rows line up with the indicator column of checkbox / radio items.

Right-click here
<ContextMenuGroup>
  <ContextMenuLabel inset>Layout</ContextMenuLabel>
  <ContextMenuItem inset>Back</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSubTrigger inset>More tools</ContextMenuSubTrigger>

Checkbox & radio items

ContextMenuCheckboxItem toggles a setting and stays open on click; ContextMenuRadioGroup wraps ContextMenuRadioItems for single-select.

<ContextMenuCheckboxItem checked={showGrid} onCheckedChange={setShowGrid}>
  Show grid
</ContextMenuCheckboxItem>

<ContextMenuRadioGroup value={role} onValueChange={setRole}>
  <ContextMenuRadioItem value="admin">Admin</ContextMenuRadioItem>
  <ContextMenuRadioItem value="member">Member</ContextMenuRadioItem>
</ContextMenuRadioGroup>

Nest a ContextMenuSub to reveal a second-level popup. The trigger shows a trailing chevron and opens on hover or with the arrow key.

<ContextMenuSub>
  <ContextMenuSubTrigger>Invite users</ContextMenuSubTrigger>
  <ContextMenuSubContent>
    <ContextMenuItem>Email</ContextMenuItem>
    <ContextMenuItem>Message</ContextMenuItem>
  </ContextMenuSubContent>
</ContextMenuSub>

API Reference

ContextMenu

ContextMenu adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuTrigger

ContextMenuTrigger adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on ContextMenuTrigger

AttributeValues
data-slot"context-menu-trigger"

ContextMenuGroup

ContextMenuGroup adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuSub

ContextMenuSub adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuRadioGroup

ContextMenuRadioGroup adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuContent

PropTypeDefaultDescription
alignAlign'start'Alignment relative to the anchor along the chosen side.
collisionPaddingPadding8Padding from the collision boundary so the popup never touches the viewport edge.
portalPropsOmit<MenuPortalProps, "children">Props forwarded to the underlying Base UI Portal.
positionerPropsOmit<MenuPositionerProps, "align" | "children" | "collisionPadding" | "side" | "sideOffset">Props forwarded to the underlying Base UI Positioner.
sideSide'bottom'Which side of the anchor to render against. May flip to avoid collisions.
sideOffsetnumber | OffsetFunction4Distance in pixels between the anchor and the popup.

ContextMenuItem

PropTypeDefaultDescription
insetbooleanfalseAdds inline-start padding so the label aligns with rows that carry a leading indicator.
toneMenuItemTone"default"destructive tints the row for delete/remove actions.

ContextMenuCheckboxItem

ContextMenuCheckboxItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuRadioItem

ContextMenuRadioItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuLabel

PropTypeDefaultDescription
insetbooleanfalseIndents the label to line up with inset rows.

ContextMenuSeparator

ContextMenuSeparator adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuShortcut

ContextMenuShortcut adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

ContextMenuSubTrigger

PropTypeDefaultDescription
insetbooleanfalseIndents the trigger to line up with inset rows.

ContextMenuSubContent

PropTypeDefaultDescription
alignAlign'start'Alignment relative to the anchor along the chosen side.
collisionPaddingPadding8Padding from the collision boundary so the popup never touches the viewport edge.
portalPropsOmit<MenuPortalProps, "children">Props forwarded to the underlying Base UI Portal.
positionerPropsOmit<MenuPositionerProps, "align" | "children" | "collisionPadding" | "side" | "sideOffset">Props forwarded to the underlying Base UI Positioner.
sideSide'bottom'Which side of the anchor to render against. May flip to avoid collisions.
sideOffsetnumber | OffsetFunction4Distance in pixels between the anchor and the popup.

Accessibility

  • The popup renders with role="menu"; items are menuitem, menuitemcheckbox, and menuitemradio. A ContextMenuLabel inside a group becomes that group's accessible name.
  • Open with a right-click or a long-press; the keyboard Menu/Shift+F10 shortcut also opens the menu when the trigger is focused.
  • Full keyboard support via Base UI's roving focus; the highlighted item uses data-highlighted (bg-accent text-accent-foreground) — there is no outline: none without a visible focus state.
  • The menu is modal: page scroll locks and focus is trapped while open, then returns to the trigger on close.
  • Icons inside items are decorative; the item's text carries the meaning.
KeyAction
Right-click / long-press / Shift+F10Open the menu on the target.
/ Move between items (wraps at the ends).
Open the focused submenu.
/ EscClose the current submenu (or the whole menu).
Enter / SpaceActivate the focused item.
AZTypeahead — jump to the next item matching the typed letters.
ContractStates tested
Behaviourdefault, active, checked, disabled, error, open, selected
Accessibilitykeyboard, labeled
Visualdefault, checked, error

Do / Don't

Do
Mirror the actions a user expects on the target (copy, rename, delete) and put destructive actions last, behind a separator.
Don't
Hide primary actions only behind a right-click — surface essential actions in visible UI too, since context menus aren't discoverable on touch.

On this page