Photon Design System

Context Menu

Actions for a specific element, opened by right-click (or long-press). Built on Base UI ContextMenu; the popup shares its look and keyboard behavior with Menu. Because it’s hidden until invoked, a context menu is always a shortcut — mirror its actions somewhere visible.

Import

import { ContextMenu } from "@photon-ai/pho-ui/components/context-menu";

Basic

ContextMenu.Trigger wraps the area that responds to right-click; the popup opens at the pointer. The rest mirrors Menu — Items, a Separator, and color="destructive" for a destructive action.

Right-click anywhere in this area
<ContextMenu.Root>
  <ContextMenu.Trigger className="…">Right-click here</ContextMenu.Trigger>
  <ContextMenu.Portal>
    <ContextMenu.Positioner>
      <ContextMenu.Popup>
        <ContextMenu.Item>Copy</ContextMenu.Item>
        <ContextMenu.Item>Paste</ContextMenu.Item>
        <ContextMenu.Separator />
        <ContextMenu.Item color="destructive">Delete</ContextMenu.Item>
      </ContextMenu.Popup>
    </ContextMenu.Positioner>
  </ContextMenu.Portal>
</ContextMenu.Root>

Groups and submenus

Cluster related actions under a ContextMenu.Group with a GroupLabel, and nest a ContextMenu.SubmenuRoot for a second level. The submenu opens on hover or Arrow Right and shares the parent’s styling.

Right-click for grouped actions
<ContextMenu.Popup>
  <ContextMenu.Group>
    <ContextMenu.GroupLabel>Edit</ContextMenu.GroupLabel>
    <ContextMenu.Item>Copy</ContextMenu.Item>
    <ContextMenu.Item>Paste</ContextMenu.Item>
  </ContextMenu.Group>
  <ContextMenu.Separator />
  <ContextMenu.SubmenuRoot>
    <ContextMenu.SubmenuTrigger>
      Share
      <CaretRight className="ml-auto" />
    </ContextMenu.SubmenuTrigger>
    <ContextMenu.Portal>
      <ContextMenu.Positioner>
        <ContextMenu.Popup>
          <ContextMenu.Item>Copy link</ContextMenu.Item>
          <ContextMenu.Item>Email</ContextMenu.Item>
        </ContextMenu.Popup>
      </ContextMenu.Positioner>
    </ContextMenu.Portal>
  </ContextMenu.SubmenuRoot>
</ContextMenu.Popup>

Anatomy

  • ContextMenu.Root — owns open state; opens on the right-click / long-press
  • ContextMenu.Trigger — the area that responds to right-click (a <div>)
  • ContextMenu.Portal → Positioner → Popup — the floating panel, anchored at the pointer
  • ContextMenu.Item — an action; Group / GroupLabel / Separator organize the list
  • ContextMenu.SubmenuRoot + .SubmenuTrigger — a nested submenu
  • ContextMenu.RadioGroup — owns a selected value (pair with Menu’s RadioItem)

Props

Every part also accepts className and render — Base UI’s prop for swapping the element it renders. ContextMenu reuses Menu’s parts, so Item, Positioner, and the submenu parts take the same props documented on Menu.

ContextMenu.Root (holds state; renders no element)

  • open — boolean — controlled open state
  • defaultOpen — boolean (default false) — initial open state when uncontrolled
  • onOpenChange — (open, details) => void — fires when the menu opens or closes
  • onOpenChangeComplete — (open) => void — fires after the open/close animation settles
  • loopFocus — boolean (default true) — wrap arrow-key focus at the ends
  • orientation — horizontal · vertical (default vertical) — arrow-key axis
  • disabled — boolean (default false) — ignore all interaction
  • closeParentOnEsc — boolean (default false) — in a submenu, Escape closes the whole menu

A context menu is always modal, so — unlike Menu — it has no modal, openOnHover, or delay prop.

ContextMenu.Trigger (<div>)

  • takes only className / render plus the div’s native props; gets data-open while its menu is showing

ContextMenu.Positioner (<div>)

  • side — top · right · bottom · left — side of the pointer to place the popup
  • align — start · center · end (default center) — alignment along that side
  • sideOffset — number (default 0) — gap from the anchor point
  • alignOffset — number (default 0) — shift along the alignment axis

ContextMenu.Item (<div role="menuitem">)

  • onClick — (event) => void — the action to run
  • closeOnClick — boolean (default true) — close the menu when clicked
  • disabled — boolean (default false)
  • label — string — overrides the text used for typeahead matching

Accessibility

  • Semantics — the popup is role="menu" and items are role="menuitem"; a GroupLabel labels its Group via aria-labelledby. The trigger is a plain region — it carries no aria-haspopup, since a context menu is invoked by right-click rather than announced as a button that opens a menu.
  • Opening — right-click (the native contextmenu event) or long-press on touch. The keyboard Menu / Shift+F10 key also fires contextmenu, so keyboard users can open it on the focused element.
  • Keyboard (once open)
    • Arrow Up / Arrow Down move the highlight (looping by default)
    • Arrow Right opens a submenu; Arrow Left closes it
    • Enter / Space activate the highlighted item
    • Typeahead: typing characters jumps to the matching item
    • Home / End jump to the first / last item
    • Escape closes the menu (a submenu first, then its parent)
  • Focus — opening moves focus into the popup and traps it (a context menu is modal); closing returns focus to where it was. The highlighted item carries data-highlighted for styling.

Best practices

  • A context menu must be a shortcut, never the only path — mirror every action in a visible Menu, toolbar, or button so it’s discoverable.
  • Scope items to the right-clicked element, and keep the list short.
  • On touch it opens via long-press; make sure the same actions are reachable by tap elsewhere.
  • Mark destructive items color="destructive" and separate them from routine actions so a right-click can’t fat-finger a delete.