Ant Design 垂直菜单(mode="vertical")详解:让子菜单以弹出浮层形式呈现
导读
本篇文章以仓库中 Menu 垂直菜单示例 及其配套 示例源码 为线索,系统讲解 Ant Design(antd)Menu 组件 vertical 垂直模式的运行机制。你将掌握:vertical 与 inline、horizontal 三种模式在子菜单呈现方式上的本质差异、基于 items 数据结构组织多级子菜单与分组的写法、以及弹出式子菜单的交互参数(触发方式、开关延时、浮层样式)和底层实现。读完即可在自己的侧边导航/站点目录场景中写出可运行的垂直菜单。
示例背景:一段被“浓缩”的官方描述
components/menu/demo/vertical.md 是官方文档中垂直菜单示例的说明文案,全文非常精炼:
- 中文:子菜单是弹出的形式。
- 英文:Submenus open as pop-ups.
一句话点透了 vertical 模式区别于其他模式的唯一关键行为:处于垂直模式的菜单,其子菜单并不像 inline 模式那样在页面布局中内嵌展开,而是以“弹出浮层”(popup)的形式悬浮在菜单旁。在 Menu 组件文档 的代码演示列表中,该示例被标记为「垂直菜单」,同时还有「顶部导航」(horizontal)、「内嵌菜单」(inline)等兄弟示例,共同覆盖了 antd 支持的三种菜单模式。
完整示例代码与逐段拆解
与所有官方示例一样,说明文案对应的可运行代码存放在同目录下的 vertical.tsx 中:
import React from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Menu } from 'antd';
type MenuItem = Required<MenuProps>['items'][number];
const items: MenuItem[] = [
{
key: 'sub1',
icon: <MailOutlined />,
label: 'Navigation One',
children: [
{
key: '1-1',
label: 'Item 1',
type: 'group',
children: [
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' },
],
},
{
key: '1-2',
label: 'Item 2',
type: 'group',
children: [
{ key: '3', label: 'Option 3' },
{ key: '4', label: 'Option 4' },
],
},
],
},
{
key: 'sub2',
icon: <AppstoreOutlined />,
label: 'Navigation Two',
children: [
{ key: '5', label: 'Option 5' },
{ key: '6', label: 'Option 6' },
{
key: 'sub3',
label: 'Submenu',
children: [
{ key: '7', label: 'Option 7' },
{ key: '8', label: 'Option 8' },
],
},
],
},
{
key: 'sub4',
label: 'Navigation Three',
icon: <SettingOutlined />,
children: [
{ key: '9', label: 'Option 9' },
{ key: '10', label: 'Option 10' },
{ key: '11', label: 'Option 11' },
{ key: '12', label: 'Option 12' },
],
},
];
const onClick: MenuProps['onClick'] = (e) => {
console.log('click', e);
};
const App: React.FC = () => (
<Menu onClick={onClick} style={{ width: 256 }} mode="vertical" items={items} />
);
export default App;
这段示例虽然短,却完整覆盖了垂直菜单的四个关键知识点:
- 开启垂直模式只用一个属性:
mode="vertical"。事实上 antd 的 Menu API 中mode的默认值就是vertical(见 Menu 组件 API 表),因此当你不传mode时,得到的即是本示例所演示的弹出式子菜单行为。 - 固定容器宽度:
style={{ width: 256 }}。垂直菜单通常作为侧边栏出现,需要给容器一个确定宽度,使顶层菜单项的单行布局稳定。 - 数据驱动而非 JSX 嵌套:示例通过
items数组 +type字段描述菜单结构,这是 antd 4.20.0 起推荐的写法(menu.tsx 中对应items配置项)。 - 用类型约束 items 结构:
type MenuItem = Required<MenuProps>['items'][number]从MenuProps中提取单个菜单项的联合类型,保证后续编写的每一项都满足类型检查。
vertical 与 inline、horizontal:子菜单的三种“打开方式”
mode 接受 vertical | horizontal | inline 三种取值。要理解示例文案「子菜单是弹出的形式」为何值得单独强调,需要横向对比另外两种模式:
| 模式 | 典型使用场景 | 子菜单呈现方式 | 展开方向/表现 | 相关示例 |
|---|---|---|---|---|
vertical |
侧边多级导航、站点目录 | 弹出浮层(popup) | 触发父级后浮层从菜单一侧弹出,可继续在浮层中嵌套子菜单 | 垂直菜单 |
inline |
侧边栏内嵌目录 | 页面内展开 | 点击后在当前布局内纵向展开,带动画收起 | 内嵌菜单 |
horizontal |
顶部一级导航 | 弹出浮层(popup) | 下拉浮层向下方展开 | 顶部导航 |
把 vertical.tsx 与 inline.tsx 对比即可发现差异的根源:两者数据结构几乎一致,差异集中在两点——
- inline 示例额外传了
defaultSelectedKeys={['1']}与defaultOpenKeys={['sub1']},因为 inline 模式下“展开”意味着真实占用页面高度,所以提供“默认展开/选中哪一级”的受控起点;而垂直模式子菜单是浮层,不挤压下方菜单项空间,示例中不设置这两个值也毫无影响。 - inline 示例在结构中插入了
{ type: 'divider' }分割线与独立的type: 'group'顶层分组,用来演示内嵌面板内的排版细节;vertical 示例则把group用在了弹出浮层内部。
文档 API 表也明确了两点差异(见 index.zh-CN.md):
popupClassName、popupOffset、popupRender等浮层定制属性说明中都标注「mode="inline"时无效」——因为 inline 根本不产生浮层,这些属性天然只服务于 vertical/horizontal 的弹出子菜单。inlineCollapsed与inlineIndent仅对inline模式有意义;menu.tsx 中甚至对该误用直接给出 dev 警告:'inlineCollapsed' should only be used when mode is inline.(见 menu.tsx)。
items 数据模型:子菜单、分组与三级嵌套
示例的 items 用统一的对象字面量描述了三种语义节点,它们对应 interface.ts 中导出的 ItemType 联合类型:MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType。
第一层:顶层子菜单(SubMenu)。 sub1、sub2、sub4 均带 icon 与 label,并通过 children 承载下级节点。它们就是文案所说的“会弹出来”的父级——把鼠标悬停/点击在任一父级上,就会从菜单侧边弹出浮层。
第二层:分组(group)与菜单项(item)。 sub1 的浮层内部是两个 type: 'group' 分组,每个分组有自己的标题(Item 1/Item 2)与若干叶子项。分组只是视觉分隔标题,不带 key 交互行为;叶子项才携带 key(如 1、2…),作为选中、事件回传的唯一标识。文档中对 group 的写法有明确规定(见 index.zh-CN.md 的 MenuItemGroupType 章节):type: 'group' 是必填项。
第三层:子菜单内的子菜单(submenu 套 submenu)。 sub2 的浮层里嵌套了 sub3(Submenu),sub3 又含有 Option 7/8 两个叶子项。这验证了垂直模式浮层具有多级递归能力:当鼠标移入 sub3 时,会再从 sub2 的浮层侧边弹出更下一级浮层。子菜单类型在 interface.ts 的 SubMenuType 中被定义,其 children 类型依旧是 ItemType<T>[],因此递归嵌套在类型层面是被天然支持的。
需要特别说明的是分组节点在三种模式下的形态差异:在 inline 内嵌面板中,group 标题会作为常驻的段落小标题直接排在页面流中;而在 vertical 的弹出浮层中,group 标题仅作为浮层内部的分组分隔,这也是示例把 group 全部放进子菜单 children 的原因。
交互行为与事件回调:垂直模式的展开与选中
示例通过 onClick 把点击事件打印到控制台:
const onClick: MenuProps['onClick'] = (e) => {
console.log('click', e);
};
配合 Menu 的 API(见 index.zh-CN.md 的 API 表),可以梳理出垂直菜单的完整交互模型:
- 展开触发:
triggerSubMenuAction默认hover,即鼠标移入父级即弹出浮层,可改为click。考虑到移动端或触屏没有 hover,切换到 click 触发是常见适配手段。 - 开关延时:
subMenuOpenDelay默认0秒立即弹出,subMenuCloseDelay默认0.1秒,后者用于防止鼠标在移向浮层的过程中误触关闭,是弹出式子菜单特有的“防抖”参数。 - 选中与回调:叶子项可被选中(
selectable默认true)。onClick收到的参数形如{ key, keyPath, domEvent, itemData },其中keyPath会给出从叶子到根部的完整 key 链路,便于在处理点击时知道用户落在哪个子菜单层级下;此外还有onSelect、onDeselect(multiple模式)、onOpenChange等回调。 - 受控展开:若需要程序化控制哪些父级处于展开态,可用
openKeys配合onOpenChange;首次渲染态则用defaultOpenKeys。需要提醒的是,由于 vertical 浮层随鼠标出现/消失,受控openKeys往往与 hover 触发配合使用才会符合直觉,实战中更常见的做法是交给组件内部管理。
源码级佐证:弹出式子菜单是怎么“弹”出来的
Ant Design 的 Menu 是基于 @rc-component/menu 二次封装而成(见 menu.tsx)。从源码中可以找到垂直模式浮层行为的实现证据:
- 不同的子菜单展开动画:menu.tsx 为三种模式注册了不同的
defaultMotions——horizontal使用 slide-up 位移动效,inline使用initCollapseMotion(高度折叠动画),而vertical归入other分支,使用${rootPrefixCls}-zoom-big缩放动效。也就是说,垂直模式子菜单“弹出来”时伴随的是浮层缩放显现,这是它作为脱离文档流的 popup 在视觉上的直接体现。 - 子菜单通过浮层承载:antd 将 item、submenu、divider 三类节点分别映射到内部组件(
MENU_COMPONENTS),SubMenu 组件内部会把非 inline 模式的子内容渲染到基于getPopupContainer定位的浮层容器中。文档中popupClassName(定制浮层样式)、popupOffset(调整浮层与父级的偏移)、popupRender(完全重写浮层内容)等参数都作用于该容器,且均标注对inline无效,进一步印证“vertical 的子菜单 = 浮层”这一行为边界。 - 节点结构二次渲染的取舍:FAQ 中说明 Menu 会通过二次渲染收集嵌套结构信息以支持 HOC 型子节点,这也是为什么更推荐直接使用
items对象数组描述结构(本示例即采用该写法),避免 JSX 深层嵌套带来的渲染与维护成本。
实战要点小结
- 想要“侧边多级导航且子菜单悬浮展开”的效果,设置
mode="vertical"(或不传mode,因为默认即为vertical)即可,容器请给定width。 - 子菜单天然支持多级与分组:父级节点写
children,分组节点写type: 'group',叶子项保证key唯一。 - 浮层相关定制属性(
popupClassName、popupOffset、popupRender、开关延时、触发方式)只对弹出式子菜单生效;需要“页面内展开”的侧边目录请改用mode="inline",折叠需求则参考 缩起内嵌菜单示例,多模式动态切换可参考 切换菜单类型示例。 - 事件处理上统一通过
onClick/onSelect拿到{ key, keyPath, ... },垂直浮层不参与页面布局,天然适合作为不挤压内容区的多级导航方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00