Menu
menu用下拉菜单收纳操作项,支持分组和危险项
用法
基础用法
Trigger 用 render 接管任意触发元素,Content 内放 MenuItem,Separator 分隔。
<Menu>
<MenuTrigger render={<Button variant="outline">菜单</Button>} />
<MenuContent>
<MenuItem>编辑</MenuItem>
<MenuItem>复制</MenuItem>
<MenuSeparator />
<MenuItem variant="danger">删除</MenuItem>
</MenuContent>
</Menu>禁用项
MenuItem 加 disabled 后不可高亮、不可点击。
<Menu>
<MenuTrigger render={<Button variant="outline">菜单</Button>} />
<MenuContent>
<MenuItem>编辑</MenuItem>
<MenuItem disabled>归档(禁用)</MenuItem>
</MenuContent>
</Menu>分组
MenuGroup + MenuGroupLabel 给一组菜单项加小标题。
<Menu>
<MenuTrigger render={<Button variant="outline">菜单</Button>} />
<MenuContent>
<MenuGroup>
<MenuGroupLabel>操作</MenuGroupLabel>
<MenuItem>编辑</MenuItem>
<MenuItem>复制</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuItem variant="danger">删除</MenuItem>
</MenuContent>
</Menu>勾选项与单选项
MenuCheckboxItem 是可开关的设置(role=menuitemcheckbox),MenuRadioGroup + MenuRadioItem 是一组互斥选项(role=menuitemradio);两者都带 aria-checked,读屏能报出当前选中项——用 MenuItem 自画一个勾看起来一样,但这层语义会丢。默认点击不关闭菜单,想选完即收传 closeOnClick。
<MenuContent>
<MenuGroup>
<MenuGroupLabel>显示</MenuGroupLabel>
<MenuCheckboxItem defaultChecked>显示网格</MenuCheckboxItem>
<MenuCheckboxItem>显示标尺</MenuCheckboxItem>
</MenuGroup>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>密度</MenuGroupLabel>
<MenuRadioGroup defaultValue="comfortable">
<MenuRadioItem value="compact">紧凑</MenuRadioItem>
<MenuRadioItem value="comfortable">适中</MenuRadioItem>
<MenuRadioItem value="loose">宽松</MenuRadioItem>
</MenuRadioGroup>
</MenuGroup>
</MenuContent>级联子菜单
MenuSub 裹住 MenuSubTrigger + MenuSubContent,子面板从父项右侧展开,支持多层嵌套。选项按维度分层收纳,适合筛选这类总数几十个、拍平到一级就用不了的场景。
<MenuContent>
<MenuItem>全部任务</MenuItem>
<MenuSeparator />
<MenuSub>
<MenuSubTrigger>状态</MenuSubTrigger>
<MenuSubContent>
<MenuItem>待办</MenuItem>
<MenuItem>进行中</MenuItem>
<MenuItem>已完成</MenuItem>
</MenuSubContent>
</MenuSub>
</MenuContent>弹出方位
MenuContent 的 side / align 控制浮层相对触发器的方位。
<Menu>
<MenuTrigger render={<Button variant="outline">菜单</Button>} />
<MenuContent side="right" align="start">
<MenuItem>编辑</MenuItem>
<MenuItem>复制</MenuItem>
</MenuContent>
</Menu>何时用
点击触发器弹出的一组操作项(编辑/复制/删除等),适合表格行操作、卡片更多按钮、头像账号菜单。选项多到一级面板放不下时用 MenuSub 分层收纳。需要 hover 展开的 mega 站点导航用 NavigationMenu;多个顶层菜单横排成 File/Edit/View 菜单条用 Menubar。
导入
import { Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuGroup, MenuGroupLabel, MenuSub, MenuSubTrigger, MenuSubContent, menuItemVariants } from "@hulianui/ui"Props
MenuContent
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| side | "top" | "right" | "bottom" | "left" | "bottom" | 弹出方位 |
| align | "start" | "center" | "end" | "start" | 沿触发器的对齐 |
| sideOffset | number | - | 与触发器的间距(px) |
| className | string | - | - |
MenuItem
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| render | ReactElement | - | 渲染成另一个元素(Next <Link> / <a>),props 与 role="menuitem"、键盘漫游一并合并进去。导航型菜单项用它而不是 `onClick` + `router.push`,见下方「导航型菜单项」 |
| disabled | boolean | false | 禁用 |
| closeOnClick | boolean | true | 点击后是否关闭菜单 |
| label | string | - | 键盘 type-ahead 的文案覆盖 |
| variant | "default" | "danger" | "default" | danger 为危险操作(红色) |
| className | string | - | - |
MenuCheckboxItem
可开关的设置项,渲染 role="menuitemcheckbox" + aria-checked。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| checked | boolean | - | 是否勾选(受控)。要非受控请改用 defaultChecked |
| defaultChecked | boolean | false | 初始是否勾选(非受控) |
| disabled | boolean | false | 禁用 |
| closeOnClick | boolean | false | 点击后是否关闭菜单。勾选项默认不关,便于连续勾选 |
| label | string | - | 键盘 type-ahead 的文案覆盖 |
| variant | "default" | "danger" | "default" | danger 为危险操作(红色) |
| className | string | - | - |
MenuRadioGroup
一组互斥选项的容器;互斥关系由它维护,MenuRadioItem 必须放在它内部。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| value | string | - | 当前选中项的值(受控)。要非受控请改用 defaultValue |
| defaultValue | string | - | 初始选中项的值(非受控) |
| disabled | boolean | false | 整组禁用 |
| className | string | - | - |
MenuRadioItem
互斥选项中的一项,渲染 role="menuitemradio" + aria-checked。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| value* | string | - | 本项的值;与所在 MenuRadioGroup 的 value 相等即为选中态 |
| disabled | boolean | false | 禁用 |
| closeOnClick | boolean | false | 点击后是否关闭菜单。单选项默认不关,选完想收起菜单要显式传 true |
| label | string | - | 键盘 type-ahead 的文案覆盖 |
| variant | "default" | "danger" | "default" | danger 为危险操作(红色) |
| className | string | - | - |
MenuSubTrigger
展开级联子菜单的菜单项,右侧带 chevron。必须与 MenuSubContent 一起放在 MenuSub 内。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| disabled | boolean | false | 禁用(不可展开子菜单) |
| label | string | - | 键盘 type-ahead 的文案覆盖 |
| variant | "default" | "danger" | "default" | danger 为危险操作(红色) |
| className | string | - | - |
没有 closeOnClick:它的点击语义是「展开下一级」而不是「执行动作」。
MenuSubContent
子菜单面板,从父项右侧展开。方位固定(side="right" / align="start"),不开放 side / align / sideOffset —— 越界时由 Base UI 自动翻边。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| className | string | - | - |
MenuTrigger 用 render={<Button />} 把任意元素作触发器。MenuSub 是纯结构件,只负责把 MenuSubTrigger 与 MenuSubContent 绑成一级。
Events
MenuItem
| 事件 | 类型 | 说明 |
|---|---|---|
| onClick | MouseEventHandler<HTMLElement> | 点击回调 |
MenuCheckboxItem
| 事件 | 类型 | 说明 |
|---|---|---|
| onCheckedChange | (checked: boolean) => void | 勾选态变化回调 |
| onClick | MouseEventHandler<HTMLElement> | 点击回调 |
MenuRadioGroup
| 事件 | 类型 | 说明 |
|---|---|---|
| onValueChange | (value: string) => void | 选中值变化回调 |
MenuRadioItem
| 事件 | 类型 | 说明 |
|---|---|---|
| onClick | MouseEventHandler<HTMLElement> | 点击回调 |
Slots
MenuContent
| 插槽 | 类型 | 说明 |
|---|---|---|
| children* | ReactNode | 菜单项 |
MenuItem
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 项内容 |
MenuCheckboxItem / MenuRadioItem
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 项内容;渲染在选中标记右侧的第二列 |
MenuRadioGroup
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 一组 MenuRadioItem |
MenuSubTrigger
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 项内容;chevron 由组件补在右侧,不用自己画 |
MenuSub / MenuSubContent
| 插槽 | 类型 | 说明 |
|---|---|---|
| children* | ReactNode | MenuSub 放一个 MenuSubTrigger + 一个 MenuSubContent;MenuSubContent 放子菜单项 |
禁忌 / 坑
- 一组选项别用 `MenuItem` + 自己画一个 √。视觉上与
MenuCheckboxItem/MenuRadioItem完全一样,所以这个错误看不出来:但元素 role 退化成menuitem、没有aria-checked,读屏用户听到的是几个平级动作,听不出这是一组互斥选项、也听不出当前选的是哪个,键盘用户的选中态只剩视觉。可开关的设置用MenuCheckboxItem,互斥选项用MenuRadioGroup+MenuRadioItem。 MenuRadioItem必须放在MenuRadioGroup内 —— 互斥关系与选中值由组维护,单独放在MenuContent里不会有选中态。- 勾选标记占的是与
MenuItem首个size-4图标同宽的第一列,所以同一个菜单里混用普通项与勾选项时文字左缘是齐的;给普通项配图标时同样用size-4,别改成别的尺寸。 - [[base-ui-menu-group-label-requires-menu-group-wrapper]]:
MenuGroupLabel直接放进MenuContent(不裹MenuGroup)会在「点开菜单」那一刻抛MenuGroupRootContext is missing,触发器渲染正常但点击崩页 —— 分组标签必须包在MenuGroup里。 MenuSubTrigger与MenuSubContent必须同在一个MenuSub内,且MenuSub必须在MenuContent里。用MenuItem加一个自己画的箭头替代MenuSubTrigger是看不出问题的错误:视觉一样,但没有aria-haspopup/aria-expanded,读屏用户听不出这一项还有下一级。- 别拿
MenuContent side="right"当子面板用。它在MenuSub里确实能渲染成子菜单(内部是同一套 Portal/Positioner/Popup),但 chevron、sideOffset、展开时父项保持高亮这三样都要自己补,漏一样就与库内其它子菜单不一致 ——MenuSubContent就是把这三样固化下来的那一层。 - 菜单默认带
max-h-[min(24rem,var(--available-height))] overflow-y-auto:放得下时不产生任何视觉差异,项数一多就改为内部滚动。这是库内兜底而不是「每个消费方自己记得加」——浮层是 fixed 的,溢出视口那截既点不到、页面也滚不出来,而且只有等数据长起来才暴露(开发时 3 项、上线后 40 项)。要更矮/更高就在className上覆盖max-h-*。ContextMenu同款。
导航型菜单项
「点了会跳到另一个页面」的菜单项要用 render 渲染成真链接,不要用 onClick + router.push:
<MenuItem render={<Link href="/settings/roles" />}>角色管理</MenuItem>差别不是写法偏好,是一整片只有真 `<a href>` 才有的浏览器行为:中键点击、Cmd/Ctrl+点击开新标签、右键「在新标签页中打开」、悬停时状态栏的 href 预览。后台系统里「设置里这一项我想另开一个标签对着看」是日常操作。劫持 click 的一方得把这些逐条补回来,漏一条用户就发现「这个菜单不能新标签打开」。
MenuCheckboxItem / MenuRadioItem / MenuSubTrigger 没有 render:它们的语义是「切一个状态」「展开下一级」,不是「去一个地方」。
相关
Navbar · BeianFooter · NavMenu · NavigationMenu · Menubar · Dock
Playground
<Menu>
<MenuTrigger render={<Button>菜单</Button>} />
<MenuContent side="bottom" align="start">
<MenuItem>编辑</MenuItem>
<MenuSeparator />
<MenuItem variant="danger">删除</MenuItem>
</MenuContent>
</Menu>