{"slug":"menu","name":"Menu","packageName":"@liquidify/react","category":"controls","maturity":"stable","summary":"A visible, sizable trigger that, on a plain click/tap, presents a Liquid Glass popover of command rows — with labelled and unlabeled sections, leading icons, a destructive role, and drill-in submenus.","importPath":"@liquidify/react/menu","sourcePath":"packages/@liquidify/react/src/components/menu/menu.tsx","specPath":"packages/@liquidify/react/src/components/menu/spec.md","docsPath":"../liquidify-page/content/component-source/menu.md","documentationStatus":"stable","storyIds":["controls-menu--default"],"additionalExports":["LiquidColor","LiquidTintValue","MenuItem","MenuItemRole","MenuNode","MenuPlacement","MenuSection","MenuSize","MenuSubmenu","SizedControlProps"],"anatomy":[{"name":"The trigger","description":"The trigger — a native , wrapped by"},{"name":"style=\"menu\" (default)","description":"style=\"menu\" (default) — plain accent-text trigger, no chrome of its"},{"name":"style=\"button\"","description":"style=\"button\" — a button-shaped, size-scaled glass-capsule anchor"},{"name":"The panel","description":"The panel — , the portalled glass box"},{"name":"Rows","description":"Rows — the vertical list of MenuRow items"},{"name":"Section header","description":"Section header — a labelled MenuSection's caption row"},{"name":"Separator","description":"Separator — a hairline role=\"separator\" (.lq-menu-separator)"},{"name":"Submenu parent row","description":"Submenu parent row — a MenuRow carrying a trailing"}],"props":[{"name":"aria-label","type":"string | undefined","required":false,"description":"Accessible name of the panel (`role=\"menu\"`). Defaults to the trigger's accessible name.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"className","type":"string | undefined","required":false,"description":"Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"defaultOpen","type":"boolean | undefined","required":false,"description":"Uncontrolled seed for the open state.","defaultValue":"`false`","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"disabled","type":"boolean | undefined","required":false,"description":"Whole-control disable: desaturates the trigger, `aria-disabled` + `data-disabled`, removed from tab order, cannot open.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"icon","type":"ReactNode","required":false,"description":"Optional leading icon in the trigger's reserved gutter, `aria-hidden`.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"items","type":"readonly MenuNode[] | undefined","required":false,"description":"The declarative command tree; each node carries a required stable `id` (§16.6).","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"label","type":"ReactNode","required":true,"description":"Fixed trigger text/content (the SwiftUI `Menu(_ title:, content:)` analogue) — NOT a current-value display.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"labelProps","type":"LabelProps | undefined","required":false,"description":"Per-instance typography override spread last into every row's label (ADR-0028).","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"onOpenChange","type":"((open: boolean) => void) | undefined","required":false,"description":"Fires with the next open state on every open/close path.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"open","type":"boolean | undefined","required":false,"description":"Controlled open state. Passing it switches Menu to controlled mode.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"placement","type":"LogicalSide | undefined","required":false,"description":"Which side of the trigger the panel sits on. Forwarded to Popup.","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"ref","type":"Ref<HTMLButtonElement> | undefined","required":false,"description":"Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"size","type":"MenuSize | undefined","required":false,"description":"Size of the `style=\"button\"` trigger capsule (the SwiftUI `.controlSize(_:)` analogue). `mini` / `large` / `xlarge` are deferred — no authentic reference frame (spec `## Out of scope`).","defaultValue":"`\"regular\"`","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"},{"name":"style","type":"\"menu\" | \"button\" | undefined","required":false,"description":"Visual style of the trigger. `\"menu\"` (default) is a plain accent-text trigger with no chrome at rest (`.menuStyle(.automatic)` analogue); `\"button\"` is a `size`-scaled glass-capsule trigger (`.menuStyle(.button)` analogue).","defaultValue":"`\"menu\"`","declaredIn":"packages/@liquidify/react/src/components/menu/menu.tsx"}],"swiftUIMap":[{"swiftUI":"`Menu(_ title:, content:)` / `Menu(_ titleKey:, content:)`","react":"`label: ReactNode`","notes":"Fixed trigger text (e.g. `\"Actions\"`); NOT a current-value display (contrast Picker).","relationship":"direct"},{"swiftUI":"`Menu(content:, label:)`","react":"`label: ReactNode` + `icon?: ReactNode`","notes":"Custom trigger (e.g. `Label(\"Edit\", systemImage:)`); leading icon slot (`…/overview.png`), `aria-hidden`.","relationship":"direct"},{"swiftUI":"`Button(\"Copy\") { }` (item)","react":"`MenuItem` = `{ id; label; onSelect?; disabled?; icon?; role? }`","notes":"`id` required + stable (§16.6 — rows key on `id`; duplicate `id` is a dev-mode error; duplicate labels allowed). Action → `onSelect`; activation dismisses.","relationship":"direct"},{"swiftUI":"`Label(\"Copy\", systemImage:)` (item)","react":"`MenuItem.icon?: ReactNode`","notes":"Leading SF Symbol → MenuRow `leading` slot.","relationship":"direct"},{"swiftUI":"`Button(role: .destructive) { }`","react":"`MenuItem.role?: \"destructive\"`","notes":"MenuRow `destructive` → `data-role=\"destructive\"`, `--lq-color-action-destructive`, scheme-stable. The label + position also carry the meaning (ADR-0019 — not colour-only).","relationship":"direct"},{"swiftUI":"`Section(\"Edit\") { … }`","react":"`MenuSection` = `{ id; label; items }`","notes":"Labelled group: `role=\"group\"` + caption header.","relationship":"direct"},{"swiftUI":"unlabeled `Section { }`","react":"`MenuSection` with `label` omitted","notes":"Divider-only group (leading `role=\"separator\"`).","relationship":"direct"},{"swiftUI":"nested `Menu(\"Open Recent\") { … }`","react":"`MenuSubmenu` = `{ id; label; icon?; items }`","notes":"Submenu: trailing chevron, `aria-haspopup=\"menu\"` + `aria-expanded` on the parent row, drill-in.","relationship":"direct"},{"swiftUI":"`.menuStyle(.automatic)` (default)","react":"`style?: \"menu\"` (default)","notes":"Plain accent-text trigger, no chrome.","relationship":"direct"},{"swiftUI":"`.menuStyle(.button)`","react":"`style?: \"button\"`","notes":"Button-shaped, `size`-scaled trigger + compact popover.","relationship":"direct"},{"swiftUI":"`.controlSize(.small / .regular)`","react":"`size?: MenuSize` (extends `SizedControlProps`)","notes":"`MenuSize = Extract<Size, \"small\" | \"regular\">`, default `regular` — the v1 subset with reference frames.","relationship":"direct"},{"swiftUI":"`.disabled(true)`","react":"`disabled?: boolean`","notes":"Whole-control disable: `aria-disabled` + `data-disabled` on the trigger, removed from tab order, no popover (`…/disabled--rest.png`).","relationship":"direct"},{"swiftUI":"`isPresented`-equivalent (implicit)","react":"`open?` / `defaultOpen?` / `onOpenChange?`","notes":"Presentation triad (ADR-0015); controlled iff `open !== undefined`; `defaultOpen` default `false`; forwarded to `Popup`.","relationship":"direct"},{"swiftUI":"(menu anchor side)","react":"`placement?: PopupPlacement`","notes":"Forwarded to `Popup`; default the sim's below-anchor placement.","relationship":"direct"},{"swiftUI":"(menu accessible name)","react":"`aria-label?: string`","notes":"`role=\"menu\"` accessible name; defaults to the trigger's accessible name, forwarded to `Popup.Content`.","relationship":"direct"},{"swiftUI":"(per-instance typography)","react":"`labelProps?: LabelProps`","notes":"ADR-0028 seam — spread last into every row's `TextLabel` (as ContextMenu).","relationship":"direct"},{"swiftUI":"(no SwiftUI analogue)","react":"`ref?`","notes":"Forwarded to the **trigger `<button>`** (the documented semantic root, §16.4), typed `Ref<HTMLButtonElement>`, always attached (open or closed).","relationship":"web-only"},{"swiftUI":"`.tint(_:)` — **not exercised**","react":"— (declined, no `tint` prop)","notes":"Accent-only, mirroring the sim + ContextMenu (P0.B6). See Out of scope.","relationship":"direct"},{"swiftUI":"`Menu(content:label:primaryAction:)` — **not exercised**","react":"— (deferred)","notes":"Split-button form; `MenuDemo.swift` ships none. See Out of scope.","relationship":"direct"},{"swiftUI":"`.menuOrder` / `.menuActionDismissBehavior` / `.menuIndicator` / `.borderlessButton`","react":"— (deferred)","notes":"Not exercised; out of scope v1.","relationship":"direct"}],"tokens":["--lq-color-accent","--lq-color-action-destructive","--lq-color-label-primary-","--lq-color-label-secondary","--lq-color-separator","--lq-duration-","--lq-duration-quick","--lq-easing-","--lq-easing-standard","--lq-font-size-body","--lq-glass-","--lq-glass-elevation","--lq-hit-target-touch","--lq-menu-min-width","--lq-menu-pad","--lq-opacity-muted","--lq-popup-","--lq-radius-14","--lq-radius-8","--lq-radius-pill","--lq-shadow-interactive-idle","--lq-shadow-presentational","--lq-space-12","--lq-space-2","--lq-space-20","--lq-space-8"],"keyboard":[{"key":"Keyboard","action":"Keyboard (APG Menu / menu-button, via Popup + the shared roving attrs):"}],"variants":[],"variantPages":[]}