Pho Design System

Menu

A dropdown list of actions opened from a trigger. Built on Base UI Menu, with keyboard navigation, grouped items, submenus, and stateful checkbox/radio items. For a right-click menu use ContextMenu; for a set of choices that stays on screen use RadioGroup.

Import

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

Items

Every row shares the same MENU_ITEM surface (secondary at rest → primary + ghost wash when highlighted). Pick the part that matches the job:

Part Role Closes on activate?
Menu.Item Run an action (onClick) yes (default)
Menu.LinkItem Navigate (href) yes
Menu.SubmenuTrigger Open a nested menu
Menu.CheckboxItem Independent toggle no (default)
Menu.RadioItem Exclusive choice in a RadioGroup no (default)

Destructive actions use color="destructive" on Item / LinkItem — icons inherit the row ink via text-current.

Menu.Item supports async onClick the same way as Dialog.Action: return a promise to keep the menu open with a spinner and lock dismissal while pending. On resolve the menu closes when closeOnClick is true (the default); on reject it stays open. Use useMenuPending() to read the in-flight state.

<Menu.Popup>
  <Menu.Item>…</Menu.Item>
  <Menu.Item onClick={() => fetch("/api").then(/* … */)}>
    Async Promise Item
  </Menu.Item>
  <Menu.LinkItem href="/docs">…</Menu.LinkItem>
  <Menu.SubmenuTrigger>…</Menu.SubmenuTrigger>
  <Menu.CheckboxItem>…</Menu.CheckboxItem>
  <Menu.RadioGroup>
    <Menu.RadioItem value="a">…</Menu.RadioItem>
  </Menu.RadioGroup>
  <Menu.Item color="destructive">Delete</Menu.Item>
</Menu.Popup>

Basic

Compose Items with SubmenuRoot / Separator. Menu.Trigger takes render to borrow any control’s look — here an outlined Button.

<Menu.Root>
  <Menu.Trigger
    render={
      <Button variant="outlined" suffix={CaretDown}>
        Options
      </Button>
    }
  />
  <Menu.Portal>
    <Menu.Positioner align="start">
      <Menu.Popup>
        <Menu.Item>
          <PencilSimple />
          Edit
        </Menu.Item>
        <Menu.SubmenuRoot>
          <Menu.SubmenuTrigger>
            Share
            <CaretRight className="ml-auto" />
          </Menu.SubmenuTrigger>
          <Menu.Portal>…</Menu.Portal>
        </Menu.SubmenuRoot>
        <Menu.Separator />
        <Menu.Item color="destructive">Delete</Menu.Item>
      </Menu.Popup>
    </Menu.Positioner>
  </Menu.Portal>
</Menu.Root>

Checkbox and radio items

For settings that live inside the menu, use Menu.CheckboxItem for independent toggles and Menu.RadioGroup + Menu.RadioItem for a single choice. Each pairs with an Indicator that renders only when selected; both are stateful and stay open on click (closeOnClick defaults to false), so a run of toggles doesn’t dismiss the menu.

<Menu.Group>
  <Menu.GroupLabel>Editor</Menu.GroupLabel>
  <Menu.CheckboxItem checked={wrap} onCheckedChange={setWrap}>
    <Menu.CheckboxItemIndicator>
      <Check />
    </Menu.CheckboxItemIndicator>
    Wrap lines
  </Menu.CheckboxItem>
</Menu.Group>
<Menu.Separator />
<Menu.RadioGroup value={density} onValueChange={setDensity}>
  <Menu.GroupLabel>Density</Menu.GroupLabel>
  <Menu.RadioItem value="comfortable">
    <Menu.RadioItemIndicator>
      <Check />
    </Menu.RadioItemIndicator>
    Comfortable
  </Menu.RadioItem>
</Menu.RadioGroup>

Anatomy

Props

Every part also accepts className and render — Base UI’s prop for swapping the element it renders (e.g. a Button for the trigger).

Menu.Root (holds state; renders no element)

Menu.Trigger (<button>)

Menu.Positioner (<div>)

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

Menu.LinkItem (<a role="menuitem">)

Menu.SubmenuTrigger (<div role="menuitem">)

Menu.CheckboxItem (<div role="menuitemcheckbox">)

Menu.RadioGroup (<div role="group">)

Menu.RadioItem (<div role="menuitemradio">)

Accessibility

Keyboard:

Best practices