Propel

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 sizes itself by default (sizing="menu"): never narrower than 192px, never wider than 320px, and at most 384px tall before the list scrolls. sm / md / lg pin the fixed picker widths (256 / 288 / 384px), anchor matches the trigger’s width, and auto hugs the content.

Show code
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 { Copy, ExternalLink, Pencil, Trash2 } from "lucide-react";

export default function BasicDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Actions"
      />
      <MenuContent>
        <MenuItem variant="accent" icon={<Icon icon={Pencil} />} label="Edit" />
        <MenuItem icon={<Icon icon={Copy} tint="secondary" />} label="Make a copy" />
        <MenuItem icon={<Icon icon={ExternalLink} tint="secondary" />} label="Open in new tab" />
        <MenuSeparator />
        <MenuItem variant="danger" icon={<Icon icon={Trash2} />} label="Delete" />
      </MenuContent>
    </Menu>
  );
}

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 { Copy, ExternalLink, Pencil, Trash2 } from "lucide-react";

export default function BasicDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Actions"
      />
      <MenuContent>
        <MenuItem variant="accent" icon={<Icon icon={Pencil} />} label="Edit" />
        <MenuItem icon={<Icon icon={Copy} tint="secondary" />} label="Make a copy" />
        <MenuItem icon={<Icon icon={ExternalLink} tint="secondary" />} label="Open in new tab" />
        <MenuSeparator />
        <MenuItem variant="danger" icon={<Icon icon={Trash2} />} 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 code
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.

Show code
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>
  );
}

With description

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

Show code
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 { Globe, Lock } from "lucide-react";
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={Lock} tint="secondary" />}
          description="Accessible only by invite"
          selected={selected === "private"}
          closeOnClick={false}
          onClick={() => setSelected("private")}
          label="Private"
        />
        <MenuItem
          icon={<Icon icon={Globe} 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 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 code
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 { Copy, ExternalLink, Trash2 } from "lucide-react";

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

MenuSubmenu nests a MenuSubmenuContent behind a MenuSubmenuTrigger row.

Show code
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 {
  Circle,
  CircleCheck,
  CircleDashed,
  CircleDot,
  CircleX,
  SignalHigh,
  SignalLow,
  SignalMedium,
} from "lucide-react";

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 sizing="menu">
            <MenuItem
              icon={<Icon icon={SignalHigh} tint="secondary" />}
              closeOnClick={false}
              label="Urgent"
            />
            <MenuItem
              icon={<Icon icon={SignalHigh} tint="secondary" />}
              closeOnClick={false}
              label="High"
            />
            <MenuItem
              icon={<Icon icon={SignalMedium} tint="secondary" />}
              closeOnClick={false}
              label="Medium"
            />
            <MenuItem
              icon={<Icon icon={SignalLow} tint="secondary" />}
              closeOnClick={false}
              label="Low"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
        <MenuSubmenu>
          <MenuSubmenuTrigger
            trailing={<Badge size="xs" variant="neutral" label="5" />}
            label="State"
          />
          <MenuSubmenuContent sizing="menu">
            <MenuItem
              icon={<Icon icon={CircleDashed} tint="secondary" />}
              closeOnClick={false}
              label="Backlog"
            />
            <MenuItem
              icon={<Icon icon={Circle} tint="secondary" />}
              closeOnClick={false}
              label="Todo"
            />
            <MenuItem
              icon={<Icon icon={CircleDot} tint="secondary" />}
              closeOnClick={false}
              label="In progress"
            />
            <MenuItem
              icon={<Icon icon={CircleCheck} tint="secondary" />}
              closeOnClick={false}
              label="Done"
            />
            <MenuItem
              icon={<Icon icon={CircleX} tint="secondary" />}
              closeOnClick={false}
              label="Cancelled"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
      </MenuContent>
    </Menu>
  );
}

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 code
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 { BookOpen, Braces, Users } from "lucide-react";

export default function LinkItemsDemo() {
  return (
    <Menu>
      <Button
        stretch="auto"
        variant="secondary"
        size="sm"
        render={<MenuTrigger />}
        label="Resources"
      />
      <MenuContent>
        <MenuLinkItem
          icon={<Icon icon={BookOpen} tint="secondary" />}
          href="#guides"
          label="Guides"
        />
        <MenuLinkItem
          icon={<Icon icon={Braces} tint="secondary" />}
          href="#api-reference"
          external
          label="API reference"
        />
        <MenuLinkItem
          icon={<Icon icon={Users} 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.

Show code
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 sizing="sm">
        <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>
  );
}

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

Show code
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.

Show code
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 { Copy, Pencil, Trash2 } from "lucide-react";

// 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={Pencil} tint="secondary" />} label="Edit" />
          <MenuItem icon={<Icon icon={Copy} tint="secondary" />} label="Make a copy" />
          <MenuItem variant="danger" icon={<Icon icon={Trash2} />} 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.

Show code
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>
  );
}

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.

PropTypeDefaultDescription
openbooleanWhether the menu is open. Controlled; pair with `onOpenChange`.
defaultOpenbooleanfalseWhether 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.
modalbooleanfalseModal behavior while open.
childrenReactNode | 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.

PropTypeDefaultDescription
renderReactElement<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.

PropTypeDefaultDescription
footerReactNodeSticky chrome pinned below the `role="menu"` popup, e.g. a `MenuFooter`.
searchReactNodeA 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"startAlignment of the menu relative to the trigger along `side`.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the menu along the trigger's edge without changing `align`.
collisionBoundaryBoundaryThe element or rect the menu is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat 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.
stickybooleanfalseKeep 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.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullPosition 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.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"bottomWhich side of the trigger the menu opens toward.
sideOffsetnumber | OffsetFunction6Distance in px between the trigger and the menu.
collisionPaddingPadding8Minimum gap in px between the menu and the viewport edge it would otherwise touch.
sizing"menu" | "anchor" | "auto" | "sm" | "md" | "lg"menuHow the menu popup is sized — width for every value, plus a height cap for `menu`. `menu` is the dropdown's own 192–320px width range with a 384px height cap, `anchor` matches the trigger's width, `auto` hugs the content with a small floor, and `sm`/`md`/`lg` are the fixed picker widths (256/288/384px). Unlike `MenuSubmenuContent`, this surface always resolves to a sizing — the default applies to an omitted _and_ an explicitly `undefined` value.
heightBehavior"flip" | "fit"fitWhat 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 a leading single-select check, an optional leading icon, the label, a secondary line, and trailing content.

PropTypeDefaultDescription
label(required)stringPrimary row label.
renderReactElement<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.
iconReactNodeLeading element before the label, e.g. `<Icon icon={Settings} tint="secondary" />`.
descriptionstringMuted secondary line under the label.
secondaryTextstringMuted text shown inline after the label.
trailingReactElement<unknown, string | JSXElementConstructor<any>>Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`.
selectedbooleanSingle-select selected state. Pass it on **every** row of a single-select list (`true` or `false`) so each row reserves the leading check gutter and stays aligned; omit it entirely on plain action rows, which then render no gutter.

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` + a lucide `Check`), 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.

PropTypeDefaultDescription
label(required)stringPrimary 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.
iconReactNodeLeading element shown after the checkbox, e.g. `<Icon icon={Settings} tint="secondary" />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>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.

PropTypeDefaultDescription
childrenReactNodeThe content of the component.
renderReactElement<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.

MenuRadioItem

The ready-made single-select menu row: grafts Base UI's `Menu.RadioItem` behavior onto the styled `MenuRadioItem` and lays out the radio dot (a kept-mounted `Menu.RadioItemIndicator` grafted onto `MenuRadioItemIndicator`), optional leading icon, label, and end content. Wrap rows in a `MenuRadioGroup` carrying `value`/`onValueChange`; the dot reads the selected state from context.

PropTypeDefaultDescription
label(required)stringPrimary 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.
iconReactNodeLeading element shown after the radio dot, e.g. `<Icon icon={Settings} tint="secondary" />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>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.

PropTypeDefaultDescription
label(required)stringPrimary 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.
iconReactNodeLeading element before the label, e.g. `<Icon icon={ExternalLink} tint="secondary" />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>Content pinned at the row's inline end, e.g. `<Shortcut keys="⌘ K" />` or a count `<Badge />`.
externalbooleanfalseThe 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).

PropTypeDefaultDescription
childrenReactNodeThe content of the component.
renderReactElement<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.

PropTypeDefaultDescription
childrenReactNodeThe heading text.
metaReactNodeContent 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).

PropTypeDefaultDescription
openbooleanWhether the submenu is open. Controlled; pair with `onOpenChange`.
defaultOpenbooleanfalseWhether 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.
childrenReactNodeThe 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.

PropTypeDefaultDescription
label(required)stringPrimary 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.
iconReactNodeLeading element before the label, e.g. `<Icon icon={Folder} tint="secondary" />`.
trailingReactElement<unknown, string | JSXElementConstructor<any>>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.

PropTypeDefaultDescription
footerReactNodeSticky chrome pinned below the `role="menu"` popup, e.g. a `MenuFooter`.
searchReactNodeA 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.
sideOffsetnumber | OffsetFunction4Distance in px between the trigger and the menu.
align"center" | "start" | "end"startAlignment of the menu relative to the trigger along `side`.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the menu along the trigger's edge without changing `align`.
collisionPaddingPaddingMinimum gap in px between the menu and the viewport edge it would otherwise touch.
collisionBoundaryBoundaryThe element or rect the menu is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat 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.
stickybooleanfalseKeep 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.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullPosition 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.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
sizing"menu" | "anchor" | "auto" | "sm" | "md" | "lg"How the menu popup is sized — width for every value, plus a height cap for `menu`. `menu` is the dropdown's own 192–320px width range with a 384px height cap, `anchor` matches the trigger's width, `auto` hugs the content with a small floor, and `sm`/`md`/`lg` are the fixed picker widths (256/288/384px). Omit it to hug the content with no floor at all.
side"top" | "right" | "bottom" | "left" | "inline-end" | "inline-start"rightWhich side of the parent row the submenu opens toward.

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.

PropTypeDefaultDescription
placeholderstringPlaceholder text.
valuestringCurrent search text. Omit it to let the field manage its own state (see `defaultValue`).
defaultValuestringInitial 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`).
resetOnClosebooleantrueClear 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"`).

PropTypeDefaultDescription
renderReactElement<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.

PropTypeDefaultDescription
childrenReactNodeThe content to render inside the transition container.