一个 React 菜单,采用 Liquid Glass 浮层,支持分组、分隔线、图标、快捷键与受控用法。
Menu 内部负责触发器、Portal、定位器、玻璃浮层、列表、操作项、分组与分隔线。默认情况下,选中操作项后会关闭菜单并调用对应的 onSelect。
操作项通过 items 数组传入。使用 type: "group" 创建带标题的分组,使用 type: "separator" 创建分隔线。每个操作项都需要唯一的 value 作为 React key;当可见标签不是纯文本时,请通过 textValue 提供键盘导航文本。
示例组合了分组操作、分隔线、图标、快捷键与禁用项。触发器下方的状态文本使用 aria-live="polite",屏幕阅读器会播报选中的操作。
No action selected.
side、align、sideOffset 与 alignOffset 控制浮层位置。空间不足时,菜单会自动翻转或偏移。
传入 open 与 onOpenChange 接管显示状态。除非同时从外部控制,触发器仍会切换菜单。
The menu is closed.
Menu 遵循菜单按钮模式:触发器暴露 aria-haspopup="menu"、aria-expanded 与 aria-controls;内部列表带有 role="menu",每个操作项都是 menuitem。ArrowDown 与 ArrowUp 在启用的操作项之间移动,typeahead 会匹配操作项标签。Enter 或 Space 激活高亮项,Escape 关闭菜单并把焦点返回触发器。
禁用的操作项仍会被播报,但无法激活。value 只作为 React key。当 label 不是纯文本时,请传入 textValue 供 typeahead 使用,并在操作项需要可访问名称时传入 aria-label 或 aria-labelledby。共享交互模式见可访问性指南。
Menu
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
trigger | ReactElement | — | 打开菜单的触发器。必填,必须转发 props 与 ref。 |
items | readonly (MenuItemProps | MenuGroupProps | MenuSeparatorProps)[] | — | 菜单中的操作项、分组与分隔线。 |
side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | "bottom" | 首选方位。 |
align | "start" | "center" | "end" | "start" | 相对触发器的对齐方式。 |
sideOffset | number | OffsetFunction | 8 | 触发器与菜单的像素距离。 |
alignOffset | number | OffsetFunction | 0 | 沿对齐轴的像素偏移。 |
collisionPadding | Padding | 每边 5px | 浮层与碰撞边界之间的留白。 |
open | boolean | — | 受控显示状态。 |
defaultOpen | boolean | false | 非受控时的初始显示状态。 |
onOpenChange | (open: boolean, details) => void | — | 打开或关闭时调用。 |
onOpenChangeComplete | (open: boolean) => void | — | 打开或关闭动画结束后调用。 |
modal | boolean | true | 打开时锁定外部交互。 |
loopFocus | boolean | true | 键盘焦点在菜单项之间循环。 |
highlightItemOnHover | boolean | true | 指针移到菜单项上时高亮该项。 |
orientation | "horizontal" | "vertical" | "vertical" | 方向键使用的焦点移动轴。 |
closeParentOnEsc | boolean | false | 在嵌套菜单中按 Escape 时关闭整个菜单,而不是只关闭子菜单。 |
onItemHighlighted | (item: HTMLElement | undefined, details) => void | — | 高亮项变化时调用。 |
actionsRef | RefObject<Menu.Root.Actions | null> | — | 命令式的 close 与 unmount 操作。 |
triggerId | string | null | — | 受控菜单所用触发器的 ID。 |
defaultTriggerId | string | null | — | 初始打开菜单所用触发器的 ID。 |
handle | Menu.Handle<Payload> | — | 允许外部触发器控制菜单的 handle。 |
disabled | boolean | false | 禁止打开菜单。 |
className | string | — | 合并到浮层的 Tailwind 类名。 |
style | CSSProperties | — | 内联样式,合并到与 className 相同的元素。 |
ui | { positioner?: ClassValue; popup?: ClassValue; glass?: ClassValue; overlay?: ClassValue; list?: ClassValue; item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue; group?: ClassValue; groupLabel?: ClassValue; separator?: ClassValue } | — | 按 data-slot 组织的内部槽位类名。 |
MenuItemProps
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | "item" | — | 将条目标记为操作项。未提供 group 或 separator 类型时可省略。 |
value | string | — | 该操作项唯一的 React key。必填。 |
label | ReactNode | — | 操作项中的可见内容。必填。 |
textValue | string | — | 键盘 typeahead 使用的纯文本标签;字符串标签会自动使用。 |
icon | ReactNode | — | 标签前的装饰性图标。 |
shortcut | ReactNode | — | 标签后的快捷键提示,仅作视觉展示。 |
onSelect | (event) => void | — | 选中操作项时调用。 |
disabled | boolean | false | 禁用该操作项。 |
closeOnClick | boolean | true | 选中后是否关闭菜单。 |
id | string | — | 操作项元素的 ID。 |
nativeButton | boolean | false | 是否渲染原生 <button>。 |
render | ReactElement | ((props, state) => ReactElement) | — | 替换操作项元素。 |
className | string | — | 合并到操作项的 Tailwind 类名。 |
style | CSSProperties | — | 内联样式,合并到与 className 相同的元素。 |
ui | { item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue } | — | 该操作项内部槽位的类名。 |
MenuGroupProps
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | "group" | — | 标记为分组。必填。 |
label | ReactNode | — | 分组标题。必填。 |
items | readonly MenuItemProps[] | — | 分组内的操作项。必填。 |
render | ReactElement | ((props, state) => ReactElement) | — | 替换分组元素。 |
className | string | — | 合并到分组的 Tailwind 类名。 |
style | CSSProperties | — | 内联样式,合并到与 className 相同的元素。 |
ui | { group?: ClassValue; groupLabel?: ClassValue; item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue } | — | 分组及其操作项内部槽位的类名。 |
MenuGroupProps 还会透传其余 Menu.Group 属性。
MenuSeparatorProps
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | "separator" | — | 标记为分隔线。必填。 |
render | ReactElement | ((props, state) => ReactElement) | — | 替换分隔线元素。 |
className | string | — | 合并到分隔线的 Tailwind 类名。 |
style | CSSProperties | — | 内联样式,合并到与 className 相同的元素。 |
ui | { separator?: ClassValue } | — | 分隔线槽位的类名。 |
MenuSeparatorProps 还会透传 Menu.Separator 的 render、id 等属性。
ui 的键对应下方列出的 data-slot 值。其余 Base UI Menu.Root 属性都会透传。
| 属性 | 说明 |
|---|---|
data-open | 菜单打开时存在于浮层上。 |
data-closed | 浮层关闭或正在关闭时存在。 |
data-highlighted | 键盘或指针高亮该操作项时存在。 |
data-disabled | 菜单或操作项禁用时存在。 |
data-popup-open | 菜单打开时存在于触发器上。 |
data-side | 浮层的实际方位。 |
data-align | 实际对齐方式(start、center 或 end)。 |
data-instant | 需要跳过动画时存在。 |
data-starting-style | 浮层开始进入动画时存在。 |
data-ending-style | 浮层正在退出动画时存在。 |
data-slot | 标记内部结构(positioner、popup、glass、overlay、list、item、item-icon、item-label、shortcut、group、group-label、separator)。 |
组件使用共享玻璃材质。可以在父级调整 --ui-glass-* token,用 className 控制布局,并通过 ui 按 data-slot 调整内部槽位。组件级数值位于 src/theme/menu.ts。Token 分层与预设见主题指南。