Menu

A floating list of actions opened from a trigger, from @clerk/headless. It is a headless primitive: it supplies open state, portalling, floating-ui positioning, roving-focus keyboard navigation with typeahead, submenu coordination, and ARIA wiring, but ships no styles — you bring your own CSS by targeting the data-* attributes each part emits.

Example

The demo below is intentionally unstyled — it renders the raw primitive so you can see its behavior and ARIA wiring. Open it, then use the arrow keys or type to move between items; Escape or selecting an item closes it.

Usage

import { Menu } from '@clerk/headless/menu';

<Menu.Root>
  <Menu.Trigger>Actions</Menu.Trigger>
  <Menu.Portal>
    <Menu.Positioner>
      <Menu.Popup>
        <Menu.Item
          label='Edit'
          onClick={() => {}}
        >
          Edit
        </Menu.Item>
        <Menu.Separator />
        <Menu.Item
          label='Delete'
          onClick={() => {}}
        >
          Delete
        </Menu.Item>
      </Menu.Popup>
    </Menu.Positioner>
  </Menu.Portal>
</Menu.Root>;

Controlled

const [open, setOpen] = useState(false);

<Menu.Root
  open={open}
  onOpenChange={setOpen}
>
  {/* trigger + portal */}
</Menu.Root>;

Parts

PartDefault ElementDescription
Menu.Rootnone (context)Owns open state, positioning, and the submenu tree
Menu.Trigger<button>Opens/closes the menu (renders as a menuitem when nested)
Menu.Portalnone (portal)Portals its children; renders nothing until mounted
Menu.Positioner<div>Floating-positioned container (role="menu")
Menu.Popup<div>Visual wrapper for the menu items
Menu.Item<button>A menu action (role="menuitem"); roving focus + typeahead
Menu.Separator<div>Visual divider (role="separator")
Menu.Arrow<svg>Optional arrow pointing at the trigger

All rendered parts accept a render prop for polymorphic rendering and standard HTML attributes for their default element. Menu.Arrow accepts floating-ui FloatingArrow props. Menu.Portal is optional.

Props

Menu.Root

PropTypeDefaultDescription
openbooleanControlled open state
defaultOpenbooleanfalseInitial open state (uncontrolled)
onOpenChange(open: boolean) => voidCalled when the open state changes
placementPlacement'bottom-start'Placement relative to the trigger ('right-start' when nested)
sideOffsetnumber4Gap in px between the trigger and the popup (0 when nested)

Menu.Item

PropTypeDefaultDescription
labelstring— (required)Text used for typeahead matching
disabledbooleanPrevent activation (uses aria-disabled, stays focusable)
closeOnClickbooleantrueClose the whole menu on click

Menu.Portal

PropTypeDefaultDescription
rootHTMLElement | null | RefObject<HTMLElement>Portal mount target

Menu.Trigger, Menu.Positioner, Menu.Popup, and Menu.Separator take no additional props beyond standard HTML attributes for their default element.

Styling

Each part emits data-* attributes you can target with any CSS solution:

AttributeApplies ToDescription
data-openTrigger, PopupPresent when the menu is open
data-closedTrigger, PopupPresent when closed (Popup stays mounted to exit)
data-activeItemPresent on the keyboard-highlighted item
data-disabledItemPresent when the item is disabled
data-sidePositioner, ArrowResolved side: top / bottom / left / right
data-starting-stylePopupPresent on the entering frame
data-ending-stylePopupPresent during the exit animation

The positioner exposes anchor/viewport geometry as CSS variables:

VariableDescription
--cl-anchor-widthTrigger width
--cl-anchor-heightTrigger height
--cl-available-widthAvailable width between the anchor and viewport edge
--cl-available-heightAvailable height between the anchor and viewport edge
--cl-transform-origintransform-origin pointing back toward the anchor
.cl-menu-item[data-active] {
  background: rgba(0, 0, 0, 0.06);
}