Menu
A popup list of actions or options opened from a trigger.
Rows take variant for their look — neutral (the default), accent for the primary action, and
danger for a destructive one.
MenuContent is as wide as its rows. sizing="auto" (the default) floors that at 204px and caps
it at 400px; sizing="anchor" matches the trigger. See Sizing below.
Show codeHide code
Installation
import {
Menu,
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
} from "@makeplane/propel/components/menu";Usage
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
Menu,
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
} from "@makeplane/propel/components/menu";
import {
CopyOutline,
NewTabArrowOutline,
EditOutline,
DeleteOutline,
} from "@makeplane/propel/icons";
export default function BasicDemo() {
return (
<Menu>
<Button
stretch="auto"
variant="secondary"
size="sm"
render={<MenuTrigger />}
label="Actions"
/>
<MenuContent>
<MenuItem variant="accent" icon={<Icon icon={EditOutline} />} label="Edit" />
<MenuItem icon={<Icon icon={CopyOutline} tint="secondary" />} label="Make a copy" />
<MenuItem
icon={<Icon icon={NewTabArrowOutline} tint="secondary" />}
label="Open in new tab"
/>
<MenuSeparator />
<MenuItem variant="danger" icon={<Icon icon={DeleteOutline} />} label="Delete" />
</MenuContent>
</Menu>
);
}Examples
Checkbox items
Multi-select rows built from MenuCheckboxItem. Each row keeps its own checked state and the menu stays open on click. MenuContent’s search prop pins a sticky MenuSearch field above the list; typing filters the rows. The menu focuses the field when it opens; Escape closes the menu from inside it, ArrowDown and ArrowUp move focus into the list, and the query clears when the menu closes — so a reopened menu is unfiltered (resetOnClose={false} keeps a controlled query on purpose).
Show codeHide code
Radio group
Single-select rows wrapped in a MenuRadioGroup sharing one value. The group marks the current
row with a trailing tick (marker="check", the default) — the same marker every other single-select
row in the library uses. Pass marker="radio" only when this popup also holds a checkbox group, so
the two kinds of choice don’t share a marker. That mixed case is on
Context Menu.
Show codeHide code
Single select
A selected row shows a trailing check at the row’s inline-end; forwarding closeOnClick={false}
moves the check without dismissing the menu. Pass selected on every row of a single-select list so
unselected rows keep the trailing gutter and the labels hold a common width.
Show codeHide code
With description
A description gives each row a muted second line; passing it switches the row to a taller, top-aligned layout.
Show codeHide code
Shortcuts
The trailing slot pins a Shortcut hint at the row’s inline end. The hint is decorative (aria-hidden) — put the canonical binding on the row as aria-keyshortcuts, and wire the actual key handler yourself; neither part listens for keys.
Show codeHide code
Submenu
MenuSubmenu nests a MenuSubmenuContent behind a MenuSubmenuTrigger row.
Show codeHide code
Link items
MenuLinkItem renders a real anchor, so a row can navigate while keeping menu-item behavior. Marking a row external sets the three things a link out has to carry, together: a trailing arrow, target="_blank", and rel="noreferrer noopener". Rows that stay in the app get none of it, so the arrow means something.
Show codeHide code
Group labels
MenuGroup pairs related rows under a non-interactive MenuLabel heading; a MenuSeparator divides the groups. The heading’s meta slot pins content at its inline end — a count, or a “Clear” action.
Show codeHide code
Footer
MenuContent’s footer prop pins sticky chrome below the role="menu" popup — here a MenuFooter with a hint.
Show codeHide code
Detached triggers
createMenuHandle() links triggers to a Menu defined elsewhere in the tree, so several launch points share one menu without lifting state.
Show codeHide code
Fit height
By default (heightBehavior="fit") a menu that cannot fit its full height shrinks before flipping: it keeps its side while the space there clears a 178px floor (six rows plus the panel’s chrome), scrolls, and flips only when it cannot and the other side can. heightBehavior="flip" restores Base UI’s behavior — jump to the other side of the trigger whenever that side has more room, even when this side held a usable menu. Both menus below open from the same spot in a bounded box; flip jumps above the trigger, the default stays below.
Show codeHide code
Sizing
A menu is as wide as its rows. sizing="auto" (the default) floors that at 204px, so a one-word
menu still reads as part of the family and a search field has room, and caps it at 400px or the
room beside the trigger, past which a long row truncates. The panel holds its width while it is
open: it may grow as rows arrive and never shrinks until it closes, so a searchable list keeps the
width it opened at while typing filters it down. sizing="anchor" matches the trigger’s width
instead; submenus share the same rule. Flip the first switch to give the rows a long label, and
type in the second menu to watch the panel hold. The bounds and their tokens are on the
Sizing page.
Show codeHide code
API Reference
Menu
The menu Root — Base UI's context/state provider (renders no element of its own). A behavior-only
role, so it lives in components (rules 1a, 2); the styled parts live in elements/menu and are
grafted onto Base UI behavior here.
MenuTrigger
The button that opens the menu — Base UI's Menu.Trigger passthrough. Propel ships no menu
trigger chrome (any control can open a menu), so graft the behavior onto the control you already
have via render (<MenuTrigger render={<Button …/>}>); Base UI contributes the open/close
behavior, ARIA wiring, and data-* state.
The trigger also records its element for MenuContent's default heightBehavior="fit" to
measure against. A consumer ref is chained after that recording rather than spread over it: in
the forward graft (<Button render={<MenuTrigger />} />, <Pill render={<MenuTrigger />} />)
Base UI clones this element with the button's own ref in its props, and a {...props} after
ref={record} would replace the recorder — nothing recorded, fit silently falling back to
flip. Same shape as PopoverTrigger.
MenuContent
The menu surface: Base UI's portal and positioner wrapped around an elevated, scrollable panel
holding the role="menu" popup. Optional search and footer chrome stays pinned outside the
scroll area. Rows (MenuItem, MenuGroup, MenuSeparator, …) are its children.
MenuItem
The ready-made selectable menu row: grafts Base UI's Menu.Item behavior onto the styled
MenuItem and lays out an optional leading icon, the label, a secondary line, the single-select
check, and trailing content.
MenuCheckboxItem
The ready-made toggleable multi-select menu row: grafts Base UI's Menu.CheckboxItem behavior
onto the styled MenuCheckboxItem and lays out the checkbox box (a kept-mounted
Menu.CheckboxItemIndicator grafted onto MenuCheckboxItemIndicator + TickOutline), optional
leading icon, label, and end content. Base UI's Menu.CheckboxItem tracks the checked state, so
checked / defaultChecked / onCheckedChange forward straight to it and the indicator reads
it from context.
MenuRadioGroup
Wraps MenuRadioItems sharing one value — Base UI's radio-group state, no element chrome.
MenuRadioItem
The ready-made single-select menu row: grafts Base UI's Menu.RadioItem behavior onto the styled
MenuRadioItem and marks the current row the way its MenuRadioGroup's marker says. check
(the default): a kept-mounted trailing tick (Menu.RadioItemIndicator grafted onto
MenuRadioItemCheck) that shows on the selected row, the same mark every other single-select row
in the library uses. radio: a leading ring (MenuRadioItemIndicator in a MenuItemControl
gutter) drawn on every row with only its inner dot hiding, for a popup that also holds a checkbox
group and needs the two kinds of choice told apart. keepMounted is load-bearing either way: the
indicator holds its slot and carries data-checked / data-unchecked for the styling to key
off. Wrap rows in a MenuRadioGroup carrying value/onValueChange; both the selected state
and the marker come from context.
MenuLinkItem
The ready-made navigational <a> menu row: grafts Base UI's Menu.LinkItem behavior onto the
styled MenuLinkItem and lays out an optional leading icon, label, and end content.
MenuGroup
Groups related menu items with their MenuLabel heading — Base UI's Menu.Group passthrough. A
structural role with no propel styling of its own, so it lives in components (rules 1a, 2).
MenuLabel
A non-interactive section heading for a group of menu items: grafts Base UI's Menu.GroupLabel
behavior onto the styled MenuLabel. Place it inside the MenuGroup it names, so Base UI wires
it as that group's accessible label.
MenuSeparator
Ready-made divider between groups of menu items: grafts Base UI's Menu.Separator behavior
(role="separator") onto the styled MenuSeparator element.
MenuSubmenu
The submenu Root — Base UI's SubmenuRoot context/state provider (renders no element of its
own). A behavior-only role, so it lives in components (rules 1a, 2).
MenuSubmenuTrigger
The ready-made submenu trigger row: grafts Base UI's Menu.SubmenuTrigger behavior onto the
styled MenuSubmenuTrigger and lays out an optional leading icon, label, end content, and the
submenu chevron indicator.
MenuSubmenuContent
The floating surface for a submenu — the same elevated, scrollable panel as MenuContent, but
anchored beside its parent row instead of below a trigger. Place it inside a MenuSubmenu,
alongside the MenuSubmenuTrigger that opens it.
MenuSearch
A sticky search field pinned above a MenuContent popup, via its search prop. The menu focuses
it on open (it sits outside the role="menu" popup — a menu may not own a textbox — so the
hand-off is programmatic, not Tab). Text-editing keys stay in the field, which keeps the menu's
type-ahead from stealing them; Escape closes the menu, and ArrowDown/ArrowUp move focus into the
list — first row down, last row up.
MenuFooter
A non-interactive footer pinned below a menu popup list (sticky chrome outside role="menu").
MenuViewport
The content-morph container for animated menus — Base UI's Menu.Viewport grafted onto the
shared relative viewport, so swapping popup content (e.g. drill-in panels) can transition sizes.