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
| Part | Default Element | Description |
|---|---|---|
Menu.Root | none (context) | Owns open state, positioning, and the submenu tree |
Menu.Trigger | <button> | Opens/closes the menu (renders as a menuitem when nested) |
Menu.Portal | none (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
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Initial open state (uncontrolled) |
onOpenChange | (open: boolean) => void | — | Called when the open state changes |
placement | Placement | 'bottom-start' | Placement relative to the trigger ('right-start' when nested) |
sideOffset | number | 4 | Gap in px between the trigger and the popup (0 when nested) |
Menu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — (required) | Text used for typeahead matching |
disabled | boolean | — | Prevent activation (uses aria-disabled, stays focusable) |
closeOnClick | boolean | true | Close the whole menu on click |
Menu.Portal
| Prop | Type | Default | Description |
|---|---|---|---|
root | HTMLElement | 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:
| Attribute | Applies To | Description |
|---|---|---|
data-open | Trigger, Popup | Present when the menu is open |
data-closed | Trigger, Popup | Present when closed (Popup stays mounted to exit) |
data-active | Item | Present on the keyboard-highlighted item |
data-disabled | Item | Present when the item is disabled |
data-side | Positioner, Arrow | Resolved side: top / bottom / left / right |
data-starting-style | Popup | Present on the entering frame |
data-ending-style | Popup | Present during the exit animation |
The positioner exposes anchor/viewport geometry as CSS variables:
| Variable | Description |
|---|---|
--cl-anchor-width | Trigger width |
--cl-anchor-height | Trigger height |
--cl-available-width | Available width between the anchor and viewport edge |
--cl-available-height | Available height between the anchor and viewport edge |
--cl-transform-origin | transform-origin pointing back toward the anchor |
.cl-menu-item[data-active] {
background: rgba(0, 0, 0, 0.06);
}