# 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](#sizing) below.

## Installation

```tsx
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSeparator,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
```
## Usage

```tsx
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).

```tsx
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuCheckboxItem,
  MenuContent,
  MenuSearch,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import * as React from "react";

const PROPERTIES = [
  { key: "assignee", label: "Assignee" },
  { key: "priority", label: "Priority" },
  { key: "due_date", label: "Due date" },
  { key: "labels", label: "Labels" },
  { key: "start_date", label: "Start date" },
] as const;

export default function CheckboxItemsDemo() {
  const [checked, setChecked] = React.useState<Record<string, boolean>>({
    assignee: true,
    priority: true,
  });
  const [query, setQuery] = React.useState("");
  const visible = PROPERTIES.filter((p) => p.label.toLowerCase().includes(query.toLowerCase()));
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Display properties"
      />
      <MenuContent
        search={<MenuSearch value={query} onValueChange={setQuery} placeholder="Search" />}
      >
        {visible.map((p) => (
          <MenuCheckboxItem
            key={p.key}
            checked={Boolean(checked[p.key])}
            onCheckedChange={(next) => setChecked((c) => ({ ...c, [p.key]: next }))}
            label={p.label}
          />
        ))}
      </MenuContent>
    </Menu>
  );
}
```

### 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](/components/context-menu#selection-rows).

```tsx
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuContent,
  MenuRadioGroup,
  MenuRadioItem,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import * as React from "react";

export default function RadioGroupDemo() {
  const [density, setDensity] = React.useState("comfortable");
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Density"
      />
      <MenuContent>
        <MenuRadioGroup value={density} onValueChange={setDensity}>
          <MenuRadioItem value="comfortable" closeOnClick={false} label="Comfortable" />
          <MenuRadioItem value="compact" closeOnClick={false} label="Compact" />
        </MenuRadioGroup>
      </MenuContent>
    </Menu>
  );
}
```

### 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.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import * as React from "react";

const SORT_OPTIONS = [
  { key: "manual", label: "Manual" },
  { key: "created", label: "Created date" },
  { key: "due", label: "Due date" },
  { key: "priority", label: "Priority" },
] as const;

export default function SelectedDemo() {
  const [sortBy, setSortBy] = React.useState("manual");
  return (
    <Menu>
      <Button stretch="auto" variant="secondary" size="sm" render={<MenuTrigger />} label="Sort" />
      <MenuContent>
        {SORT_OPTIONS.map((option) => (
          <MenuItem
            key={option.key}
            selected={sortBy === option.key}
            closeOnClick={false}
            onClick={() => setSortBy(option.key)}
            label={option.label}
          />
        ))}
      </MenuContent>
    </Menu>
  );
}
```

### With description

A `description` gives each row a muted second line; passing it switches the row to a taller, top-aligned layout.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { GlobeOutline, LockOutline } from "@makeplane/propel/icons";
import * as React from "react";

export default function WithDescriptionDemo() {
  const [selected, setSelected] = React.useState("private");
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Visibility"
      />
      <MenuContent>
        <MenuItem
          icon={<Icon icon={LockOutline} tint="secondary" />}
          description="Accessible only by invite"
          selected={selected === "private"}
          closeOnClick={false}
          onClick={() => setSelected("private")}
          label="Private"
        />
        <MenuItem
          icon={<Icon icon={GlobeOutline} tint="secondary" />}
          description="Anyone in the workspace except Guests can join"
          selected={selected === "public"}
          closeOnClick={false}
          onClick={() => setSelected("public")}
          label="Public"
        />
      </MenuContent>
    </Menu>
  );
}
```

### Shortcuts

The `trailing` slot pins a [`Shortcut`](/components/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.

```tsx
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 { Shortcut } from "@makeplane/propel/components/shortcut";
import { CopyOutline, NewTabArrowOutline, DeleteOutline } from "@makeplane/propel/icons";

export default function ShortcutsDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Actions"
      />
      <MenuContent>
        <MenuItem
          icon={<Icon icon={CopyOutline} tint="secondary" />}
          label="Copy link"
          aria-keyshortcuts="Meta+C"
          trailing={<Shortcut keys="⌘C" />}
        />
        <MenuItem
          icon={<Icon icon={NewTabArrowOutline} tint="secondary" />}
          label="Open in new tab"
        />
        <MenuSeparator />
        <MenuItem
          variant="danger"
          icon={<Icon icon={DeleteOutline} />}
          label="Delete"
          aria-keyshortcuts="Meta+Backspace"
          trailing={<Shortcut keys="⌘⌫" />}
        />
      </MenuContent>
    </Menu>
  );
}
```

### Submenu

`MenuSubmenu` nests a `MenuSubmenuContent` behind a `MenuSubmenuTrigger` row.

```tsx
import { Badge } from "@makeplane/propel/components/badge";
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSubmenu,
  MenuSubmenuContent,
  MenuSubmenuTrigger,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import {
  CircleOutline,
  TickCircleOutline,
  CircleDashedOutline,
  DotOutline,
  CloseCircleOutline,
  PriorityHigh,
  PriorityLow,
  PriorityMedium,
} from "@makeplane/propel/icons";

export default function SubmenuDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Filter by"
      />
      <MenuContent>
        <MenuSubmenu>
          <MenuSubmenuTrigger
            trailing={<Badge size="xs" variant="neutral" label="4" />}
            label="Priority"
          />
          <MenuSubmenuContent>
            <MenuItem
              icon={<Icon icon={PriorityHigh} tint="secondary" />}
              closeOnClick={false}
              label="Urgent"
            />
            <MenuItem
              icon={<Icon icon={PriorityHigh} tint="secondary" />}
              closeOnClick={false}
              label="High"
            />
            <MenuItem
              icon={<Icon icon={PriorityMedium} tint="secondary" />}
              closeOnClick={false}
              label="Medium"
            />
            <MenuItem
              icon={<Icon icon={PriorityLow} tint="secondary" />}
              closeOnClick={false}
              label="Low"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
        <MenuSubmenu>
          <MenuSubmenuTrigger
            trailing={<Badge size="xs" variant="neutral" label="5" />}
            label="State"
          />
          <MenuSubmenuContent>
            <MenuItem
              icon={<Icon icon={CircleDashedOutline} tint="secondary" />}
              closeOnClick={false}
              label="Backlog"
            />
            <MenuItem
              icon={<Icon icon={CircleOutline} tint="secondary" />}
              closeOnClick={false}
              label="Todo"
            />
            <MenuItem
              icon={<Icon icon={DotOutline} tint="secondary" />}
              closeOnClick={false}
              label="In progress"
            />
            <MenuItem
              icon={<Icon icon={TickCircleOutline} tint="secondary" />}
              closeOnClick={false}
              label="Done"
            />
            <MenuItem
              icon={<Icon icon={CloseCircleOutline} tint="secondary" />}
              closeOnClick={false}
              label="Cancelled"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
      </MenuContent>
    </Menu>
  );
}
```

### 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.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuLinkItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { BookClosedOutline, BracesOutline, MembersOutline } from "@makeplane/propel/icons";

export default function LinkItemsDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Resources"
      />
      <MenuContent>
        <MenuLinkItem
          icon={<Icon icon={BookClosedOutline} tint="secondary" />}
          href="#guides"
          label="Guides"
        />
        <MenuLinkItem
          icon={<Icon icon={BracesOutline} tint="secondary" />}
          href="#api-reference"
          external
          label="API reference"
        />
        <MenuLinkItem
          icon={<Icon icon={MembersOutline} tint="secondary" />}
          href="#community"
          external
          label="Community forum"
        />
      </MenuContent>
    </Menu>
  );
}
```

### 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.

```tsx
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuCheckboxItem,
  MenuContent,
  MenuGroup,
  MenuItem,
  MenuLabel,
  MenuSeparator,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import * as React from "react";

const SORTS = [
  { key: "date", label: "Date created" },
  { key: "name", label: "Name" },
  { key: "type", label: "Type" },
] as const;

const PANELS = [
  { key: "minimap", label: "Minimap" },
  { key: "search", label: "Search" },
  { key: "sidebar", label: "Sidebar" },
] as const;

export default function GroupLabelsDemo() {
  const [sort, setSort] = React.useState("date");
  const [workspace, setWorkspace] = React.useState<Record<string, boolean>>({ sidebar: true });
  const shown = Object.values(workspace).filter(Boolean).length;
  return (
    <Menu>
      <Button stretch="auto" variant="secondary" size="sm" render={<MenuTrigger />} label="View" />
      <MenuContent>
        <MenuGroup>
          <MenuLabel>Sort</MenuLabel>
          {SORTS.map((s) => (
            <MenuItem
              key={s.key}
              selected={sort === s.key}
              closeOnClick={false}
              onClick={() => setSort(s.key)}
              label={s.label}
            />
          ))}
        </MenuGroup>
        <MenuSeparator />
        <MenuGroup>
          <MenuLabel meta={`${shown} shown`}>Workspace</MenuLabel>
          {PANELS.map((p) => (
            <MenuCheckboxItem
              key={p.key}
              checked={Boolean(workspace[p.key])}
              onCheckedChange={(next) => setWorkspace((w) => ({ ...w, [p.key]: next }))}
              label={p.label}
            />
          ))}
        </MenuGroup>
      </MenuContent>
    </Menu>
  );
}
```

### Footer

`MenuContent`'s `footer` prop pins sticky chrome below the `role="menu"` popup — here a `MenuFooter` with a hint.

```tsx
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuContent,
  MenuFooter,
  MenuItem,
  MenuTrigger,
} from "@makeplane/propel/components/menu";

export default function FooterDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Assign"
      />
      <MenuContent footer={<MenuFooter>Type to search assignees.</MenuFooter>}>
        <MenuItem label="Amelia Parker" />
        <MenuItem label="David Wilson" />
        <MenuItem label="Sarah Jones" />
      </MenuContent>
    </Menu>
  );
}
```

### Detached triggers

`createMenuHandle()` links triggers to a `Menu` defined elsewhere in the tree, so several launch points share one menu without lifting state.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
  createMenuHandle,
  Menu,
  MenuContent,
  MenuItem,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import { CopyOutline, EditOutline, DeleteOutline } from "@makeplane/propel/icons";

// A handle links triggers that live far from the menu they open, so several
// launch points can share one menu without hoisting controlled state.
const actionsMenu = createMenuHandle();

export default function DetachedTriggersDemo() {
  return (
    <div className="flex items-center gap-2">
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger handle={actionsMenu} />}
        label="Actions"
      />
      <Button
        stretch="auto"
        variant="ghost"
        size="sm"
        render={<MenuTrigger handle={actionsMenu} />}
        label="More"
      />
      <Menu handle={actionsMenu}>
        <MenuContent>
          <MenuItem icon={<Icon icon={EditOutline} tint="secondary" />} label="Edit" />
          <MenuItem icon={<Icon icon={CopyOutline} tint="secondary" />} label="Make a copy" />
          <MenuItem variant="danger" icon={<Icon icon={DeleteOutline} />} label="Delete" />
        </MenuContent>
      </Menu>
    </div>
  );
}
```

### 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.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import * as React from "react";

const OPTIONS = Array.from({ length: 20 }, (_, index) => `Option ${index + 1}`);

// Both menus open from the same spot, just past the middle of a bounded box:
// enough room below for a six-row menu, slightly more above. The flip opt-out
// jumps the trigger for that slightly-more; the default stays below and scrolls.
export default function FitHeightDemo() {
  const [boundary, setBoundary] = React.useState<HTMLElement | null>(null);
  return (
    <div
      ref={setBoundary}
      className="relative w-full max-w-lg rounded-lg border border-subtle"
      style={{ height: 480 }}
    >
      <div className="absolute flex gap-2" style={{ top: 240, left: 16 }}>
        <Menu>
          <Button
            stretch="auto"
            variant="secondary"
            size="sm"
            render={<MenuTrigger />}
            label="Flip"
          />
          <MenuContent heightBehavior="flip" collisionBoundary={boundary ?? undefined}>
            {OPTIONS.map((label) => (
              <MenuItem key={label} closeOnClick={false} label={label} />
            ))}
          </MenuContent>
        </Menu>
        <Menu>
          <Button
            stretch="auto"
            variant="secondary"
            size="sm"
            render={<MenuTrigger />}
            label="Fit (default)"
          />
          <MenuContent collisionBoundary={boundary ?? undefined}>
            {OPTIONS.map((label) => (
              <MenuItem key={label} closeOnClick={false} label={label} />
            ))}
          </MenuContent>
        </Menu>
      </div>
    </div>
  );
}
```

### 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](/foundations/sizing#popup-widths) page.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSearch,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import { Switch } from "@makeplane/propel/components/switch";
import { TickCircleOutline, CircleDashedOutline, DotOutline } from "@makeplane/propel/icons";
import * as React from "react";

const SHORT_ROWS = [
  { label: "Draft", icon: CircleDashedOutline },
  { label: "In review", icon: DotOutline },
  { label: "Shipped", icon: TickCircleOutline },
];

const LONG_ROWS = [
  { label: "Draft", icon: CircleDashedOutline },
  { label: "Waiting on the platform team's sign-off", icon: DotOutline },
  { label: "Approved — ships with the next release rollout", icon: TickCircleOutline },
];

const PEOPLE = [
  "Ada",
  "Grace Hopper",
  "Katherine Johnson",
  "Margaret Hamilton — mission software lead",
  "Mary Allen Wilkes",
];

// Two menus, no rung chosen on either. The first shows the bounds: short rows sit on the 204px
// floor, long rows widen the panel to the widest of them, up to the 400px ceiling. The second
// shows the hold: the list opens at the width of its longest row and keeps it while typing
// filters the rows down.
export default function SizingDemo() {
  const [long, setLong] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const rows = long ? LONG_ROWS : SHORT_ROWS;
  const people = PEOPLE.filter((name) => name.toLowerCase().includes(query.toLowerCase()));
  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2 text-body-sm-regular text-secondary">
        <Switch
          size="md"
          aria-label="Rows outgrow the floor"
          checked={long}
          onCheckedChange={setLong}
        />
        Rows outgrow the floor
      </div>
      <div className="flex flex-wrap gap-2">
        <Menu>
          <Button
            stretch="auto"
            variant="secondary"
            size="sm"
            render={<MenuTrigger />}
            label="Status"
          />
          <MenuContent>
            {rows.map((row) => (
              <MenuItem
                key={row.label}
                icon={<Icon icon={row.icon} tint="secondary" />}
                closeOnClick={false}
                label={row.label}
              />
            ))}
          </MenuContent>
        </Menu>
        <Menu onOpenChange={(open) => (open ? undefined : setQuery(""))}>
          <Button
            stretch="auto"
            variant="secondary"
            size="sm"
            render={<MenuTrigger />}
            label="Assignee"
          />
          <MenuContent
            search={
              <MenuSearch value={query} onValueChange={setQuery} placeholder="Search people" />
            }
          >
            {people.map((name) => (
              <MenuItem key={name} closeOnClick={false} label={name} />
            ))}
            {people.length === 0 ? <MenuItem disabled label="No one matches" /> : null}
          </MenuContent>
        </Menu>
      </div>
    </div>
  );
}
```
## 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Whether the menu is open. Controlled; pair with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the menu is open on mount. Uncontrolled. |
| `onOpenChange` | `((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)` | — | Called with the next open state when the menu opens or closes. |
| `modal` | `boolean` | `true` | Whether the menu enters a modal state when open, like `Select`: page scroll is locked and a press outside the menu only dismisses it. The trigger is cut out of the backdrop. Set `false` to leave the rest of the page interactive while the menu is open. Submenus ignore this prop, and a menu opened by hover is never modal. |
| `children` | `ReactNode \| PayloadChildRenderFunction<Payload>` | — | The trigger and menu surface (`MenuTrigger`, `MenuContent`). |

### 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`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, MenuTriggerState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `footer` | `ReactNode` | — | Sticky chrome pinned below the `role="menu"` popup, e.g. a `MenuFooter`. |
| `search` | `ReactNode` | — | A search field pinned above the `role="menu"` popup, e.g. a `MenuSearch`. The menu focuses it when it opens, Escape closes from inside it, and ArrowDown/ArrowUp move focus into the list. |
| `align` | `"center" \| "start" \| "end"` | `start` | Alignment of the menu relative to the trigger along `side`. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset in px along the align axis — slides the menu along the trigger's edge without changing `align`. |
| `collisionBoundary` | `Boundary` | — | The element or rect the menu is confined to when avoiding collisions. Base UI's clipping ancestors when unset. |
| `collisionAvoidance` | `CollisionAvoidance` | — | What to do when the menu would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset. |
| `sticky` | `boolean` | `false` | Keep the menu on screen after the trigger scrolls out of view, instead of following it out. |
| `positionMethod` | `"absolute" \| "fixed"` | `"absolute"` | Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors, e.g. a trigger inside a sticky header. |
| `anchor` | `Element \| VirtualElement \| RefObject<Element \| null> \| (() => Element \| VirtualElement \| null) \| null` | — | Position against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one. Geometry only; the trigger keeps its role and interactions. |
| `disableAnchorTracking` | `boolean` | `false` | Stop tracking layout shifts of the anchor — position once, on open. |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-end" \| "inline-start"` | `bottom` | Which side of the trigger the menu opens toward. |
| `sideOffset` | `number \| OffsetFunction` | `6` | Distance in px between the trigger and the menu. |
| `collisionPadding` | `Padding` | `16` | Minimum gap in px between the menu and the viewport edge it would otherwise touch. |
| `sizing` | `"menu" \| "anchor" \| "auto" \| "sm" \| "md" \| "lg"` | `auto` | How the menu popup takes its width. `auto` is the content's width — never narrower than 204px, never wider than 400px or the room beside the trigger, and held while open so a list that filters down does not reflow. `anchor` matches the trigger's width instead, growing only with its content. The default applies to an omitted _and_ an explicitly `undefined` value. |
| `heightBehavior` | `"flip" \| "fit"` | `fit` | What decides which side of the trigger the menu opens on when space is short. `fit`, the default, shrinks before flipping: the menu keeps `side` while the usable space there clears a 178px floor (six rows plus the panel's chrome), and flips only when it does not and the other side does; when neither clears the floor, the bigger side wins. The side is resolved each time the menu opens and holds while it stays open. `flip` is Base UI's behavior: jump to the other side whenever the menu's full height does not fit and the other side has more room — even when this side held a usable menu. `fit` applies to a vertical `side`, needs the menu's own `MenuTrigger` to measure against (a menu opened without one falls back to `flip`), and defers to an explicit `collisionAvoidance`. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | Element the row renders as, when a `<div>` is not the right one — e.g. `render={<a href=… />}` for a row that navigates. The row's own styling and menu behavior still apply. Narrower than Base UI's `render`: the styled row is the graft target, so a state-aware render function has no row state to receive. |
| `variant` | `"neutral" \| "accent" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy, `accent` marks the primary action, and `danger` marks a destructive one. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={Settings} tint="secondary" />`. |
| `description` | `string` | — | Muted secondary line under the label. |
| `secondaryText` | `string` | — | Muted text shown inline after the label. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |
| `selected` | `boolean` | — | Single-select selected state. Pass it on **every** row of a single-select list (`true` or `false`) so each row reserves the trailing check gutter and its labels stay a common width; omit it entirely on plain action rows, which then render no gutter. Passing it also makes the row a `menuitemradio` with `aria-checked`, so the current value is announced rather than left to the check glyph. That is why the boolean must be real on every row of the set: a mix of `selected` and omitted rows produces a mix of `menuitemradio` and `menuitem`, which reads as one radio in a list of commands. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "accent" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy, `accent` marks the primary action, and `danger` marks a destructive one. |
| `icon` | `ReactNode` | — | Leading element shown after the checkbox, e.g. `<Icon icon={Settings} tint="secondary" />`. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |

### MenuRadioGroup

Wraps `MenuRadioItem`s sharing one `value` — Base UI's radio-group state, no element chrome.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The content of the component. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, MenuRadioGroupState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `marker` | `"radio" \| "check"` | `check` | How the group marks its current row. Defaults to `check` — the trailing tick every other single-select row uses. Pass `radio` only when this group shares a popup with a multi-select (checkbox) group and the two need telling apart. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "accent" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy, `accent` marks the primary action, and `danger` marks a destructive one. |
| `icon` | `ReactNode` | — | Leading element, e.g. `<Icon icon={Settings} tint="secondary" />`. With the group's `marker="radio"` it sits after the ring; with the default tick it leads the row. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "accent" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy, `accent` marks the primary action, and `danger` marks a destructive one. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={ExternalLink} tint="secondary" />`. |
| `trailing` | `ReactNode` | — | Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |
| `external` | `boolean` | `false` | The row leaves the app. Sets the three things a link out has to carry, together: a trailing arrow so the row says where it goes, `target="_blank"`, and `rel="noreferrer noopener"`. They are one prop because they are one decision, and because two of them are easy to forget — a missing `rel` is a security default, not a style nit. Both are overridable: pass your own `target` or `rel` and it wins. Passing `trailing` also wins, in which case the row is yours to annotate. |

### 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).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The content of the component. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, MenuGroupState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The heading text. |
| `meta` | `ReactNode` | — | Content pinned at the heading row's inline end, e.g. a count or a "Clear" action. |

### MenuSeparator

Ready-made divider between groups of menu items: grafts Base UI's `Menu.Separator` behavior
(`role="separator"`) onto the styled `MenuSeparator` element.

No component-specific props — accepts the standard HTML attributes for the element it renders.

### 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).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Whether the submenu is open. Controlled; pair with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the submenu is open on mount. Uncontrolled. |
| `onOpenChange` | `((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)` | — | Called with the next open state when the submenu opens or closes. |
| `children` | `ReactNode` | — | The submenu's trigger and nested content. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` | — | Primary row label. |
| `variant` | `"neutral" \| "accent" \| "danger"` | `"neutral"` | Row look. `neutral` is the standard text hierarchy, `accent` marks the primary action, and `danger` marks a destructive one. |
| `icon` | `ReactNode` | — | Leading element before the label, e.g. `<Icon icon={Folder} tint="secondary" />`. |
| `trailing` | `ReactNode` | — | Content pinned before the submenu chevron, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `footer` | `ReactNode` | — | Sticky chrome pinned below the `role="menu"` popup, e.g. a `MenuFooter`. |
| `search` | `ReactNode` | — | A search field pinned above the `role="menu"` popup, e.g. a `MenuSearch`. The menu focuses it when it opens, Escape closes from inside it, and ArrowDown/ArrowUp move focus into the list. |
| `sideOffset` | `number \| OffsetFunction` | `4` | Distance in px between the trigger and the menu. |
| `align` | `"center" \| "start" \| "end"` | `start` | Alignment of the menu relative to the trigger along `side`. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset in px along the align axis — slides the menu along the trigger's edge without changing `align`. |
| `collisionPadding` | `Padding` | — | Minimum gap in px between the menu and the viewport edge it would otherwise touch. |
| `collisionBoundary` | `Boundary` | — | The element or rect the menu is confined to when avoiding collisions. Base UI's clipping ancestors when unset. |
| `collisionAvoidance` | `CollisionAvoidance` | — | What to do when the menu would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset. |
| `sticky` | `boolean` | `false` | Keep the menu on screen after the trigger scrolls out of view, instead of following it out. |
| `positionMethod` | `"absolute" \| "fixed"` | `"absolute"` | Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors, e.g. a trigger inside a sticky header. |
| `anchor` | `Element \| VirtualElement \| RefObject<Element \| null> \| (() => Element \| VirtualElement \| null) \| null` | — | Position against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one. Geometry only; the trigger keeps its role and interactions. |
| `disableAnchorTracking` | `boolean` | `false` | Stop tracking layout shifts of the anchor — position once, on open. |
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-end" \| "inline-start"` | `right` | Which side of the parent row the submenu opens toward. |
| `sizing` | `"menu" \| "anchor" \| "auto" \| "sm" \| "md" \| "lg"` | `auto` | How the submenu popup takes its width — the same rule as `MenuContent`: `auto` is the content's width between the 204px floor and the 400px ceiling, held while open; `anchor` matches the parent row's width. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | — | Placeholder text. |
| `value` | `string` | — | Current search text. Omit it to let the field manage its own state (see `defaultValue`). |
| `defaultValue` | `string` | — | Initial search text when the field manages its own state. |
| `onValueChange` | `((value: string) => void)` | — | Called with the new text on each keystroke — and with `""` when the menu closes (see `resetOnClose`). |
| `resetOnClose` | `boolean` | `true` | Clear the query when the menu closes. The field's own state resets with the popup unmount; this also calls `onValueChange("")` on close so a consumer-held query — which outlives the popup — is cleared too and the list is unfiltered on reopen. Turn it off to keep a _controlled_ query across opens on purpose; an uncontrolled field (`defaultValue`) resets with the popup either way, so all the flag changes there is whether `onValueChange("")` fires. |

### MenuFooter

A non-interactive footer pinned below a menu popup list (sticky chrome outside `role="menu"`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | The content to render inside the transition container. |
