Ant Design Menu `popupRender` 实战:打造企业级多级导航的"超级下拉面板"
导读
在 Ant Design 中,当菜单项包含子菜单(SubMenu)时,horizontal、vertical 等模式下子项默认以一层简单的弹出面板呈现。若你希望把下拉内容做成"图文卡片式超级导航"(如首页顶部常见的巨型下拉、两级分类导航),就需要使用 Menu 的 popupRender 属性整体接管弹出面板的渲染。本文将基于当前仓库的官方演示与源码,讲解 popupRender 的签名与触发时机、它在 Menu 与 SubMenu 两个层级的配置方式,并结合一份可运行的完整代码,演示如何用 Flex / Row / Col、antd-style 与 ConfigProvider 组合出多列内容的下拉面板。
从官方示例说起:custom-popup-render 想要表达什么
在 Menu 官方演示目录 中,页面使用了一个 mode="horizontal" 的水平菜单,菜单数据由两个带 children 的子菜单(features 与 resources)和一个普通项(home)组成。子菜单的每一项不再是普通文本,而是一个包含标题与描述的小型内容卡片:
{
key: 'getting-started',
label: <MenuItem title="Getting Started" description="Quick start guide and learn the basics." />,
},
随后,通过 popupRender 将子菜单默认的弹出列表整体替换为一个带标题头、且子项按两列栅格排布的巨型面板。也就是说,示例演示的核心能力是:
使用
popupRender属性,把子菜单默认的弹出菜单(popup)整体替换为你自己的任意 React 节点,用于自定义子菜单弹出区域的渲染。
对应的中英文说明见 custom-popup-render.md。该演示在 Menu 中文文档 中登记为"自定义弹出框"用例。
popupRender 的 API 签名与两种设置位置
签名与参数含义
popupRender 在 antd 中的类型为:
type PopupRender = (
node: React.ReactElement,
props: { item: SubMenuProps; keys: string[] },
) => React.ReactNode;
两个入参的含义:
| 参数 | 说明 |
|---|---|
node |
子菜单默认渲染出的弹出节点(即未被替换时的默认列表),可作为回退内容或在其中插入自定义元素 |
props.item |
触发弹出面板的当前子菜单项(SubMenu)配置数据,可读取其 title、children、key 等字段 |
props.keys |
从菜单根到当前子菜单的 key 路径数组,可用于多级嵌套场景下的定位判断 |
官方示例中只使用了第二个参数,并将返回值做成全新结构,从而完全接管了面板内容:
const popupRender: MenuProps['popupRender'] = (_, { item }) => {
return (
<Flex className={styles.navigationPopup} vertical gap="medium">
<Typography.Title level={3} className={styles.leadingHeader}>
{item.title}
</Typography.Title>
<Row gutter={16}>
{React.Children.map(item.children as React.ReactNode, (child) => {
if (!React.isValidElement(child)) return null;
return (
<Col span={12} key={child.key}>
{child}
</Col>
);
})}
</Row>
</Flex>
);
};
注意 item.children 的类型是 ItemType[](见 interface.ts 中 SubMenuType 的定义),因此示例中先将其视为 ReactNode,再用 React.Children.map 配合 React.isValidElement 逐个判断、按 Col span={12} 双列排布。若子项不是合法 React 元素则返回 null 做安全兜底。
两个生效层级:Menu 级与 SubMenu 级
根据 Menu API 文档 与 SubMenu API 文档,popupRender 同时出现在两个位置:
- Menu 上:作为整个菜单的默认弹出渲染函数,作用于所有未单独覆盖的子菜单,类型为
(node, props) => ReactNode; - 单个 SubMenu 项上:仅自定义"当前这个"子菜单的弹出框,类型相同,优先级更高,可对个别子菜单做差异化定制。
这一点与仓库的实现结构一致:antd 的 menu.tsx 接收 props 后去掉与弹出无关的内部字段,把 popupRender 等能力透传给底层 @rc-component/menu(仓库依赖为 ~1.5.0,见 package.json);而带子菜单的项经由 SubMenu.tsx 包装为 RcSubMenu,antd 的封装为其附加 popupClassName(拼接 prefixCls、主题类与语义化 class)与带 zIndex 的 popupStyle 后,item 级 popupRender 会随展开逻辑一并被底层使用。因此可以推断:运行时以"最靠近当前子菜单"的配置为准,item 级覆盖会优先于 Menu 级默认值。
使用对象语法时的写法
当菜单数据由 items 提供时,把 popupRender 直接写在某个子菜单节点上即可实现"仅该子菜单使用自定义弹出面板":
const items = [
{
key: 'features',
label: 'Features',
popupRender: (node, { item }) => <YourPanel item={item} />,
children: [ /* ... */ ],
},
];
撑起"超级下拉面板"的样式与主题配套
只替换内容还不够,面板外观需要靠外层样式与主题 Token 一起配合。官方示例做了三件事,值得完整参考。
1. 用 antd-style 设计弹出面板外观
示例通过 createStyles(antd-style,也即本仓库顶层 alias 中映射的 CSS-in-JS 方案)消费主题 Token 来构造面板样式:
const useStyles = createStyles(({ token }) => ({
navigationPopup: {
padding: token.padding,
minWidth: 480,
background: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary,
},
// ...
}));
这里选取的 Token 与 antd 内置弹出层的视觉规范一致:colorBgElevated 表示浮层背景、boxShadowSecondary 为次级阴影、borderRadiusLG 是大圆角。得益于 antd v5+ 的 CSS-in-JS 架构,示例无需任何全局 CSS 文件,样式随组件按需注入,且会自动适配 ConfigProvider 的暗色/定制主题。
2. 用 ConfigProvider 收敛菜单配色
弹出面板默认主题可能与深色菜单不相称,示例通过在 ConfigProvider 中直接修改 Menu 组件级 Token,让展开后的浮层面板呈现白底、选中/悬浮高亮为品牌蓝:
<ConfigProvider
theme={{
components: {
Menu: {
popupBg: '#fff',
horizontalItemSelectedColor: '#1677ff',
horizontalItemHoverColor: '#1677ff',
},
Typography: {
titleMarginBottom: 0,
titleMarginTop: 0,
},
},
}}
>
其中 popupBg 负责弹出面板背景色;horizontalItemSelectedColor / horizontalItemHoverColor 分别控制水平模式下选中态与悬浮态文字颜色。由于面板内复用了 Typography 标题,这里顺手把 titleMarginBottom / titleMarginTop 置零,避免标题自带的上下外边距撑乱栅格布局。
3. 面板内使用 Flex + Row/Col 排版
navigationPopup 通过 Flex vertical gap="medium" 纵向排布"面板大标题 + 栅格内容区";内容区用 Row gutter={16} 与 Col span={12} 实现两列卡片布局,子项卡片本身是一个带 hover 背景反馈的 Space 结构(大标题 + 次级说明文字),从而使面板看起来像一套完整的"导航落地页"。
触发展开与适用模式的说明
popupRender主要针对弹出式展开的子菜单。在horizontal/vertical模式下,SubMenu 通过浮层展开,此时自定义面板直接生效;在inline模式下,子菜单默认以内嵌方式展开,不产生浮层弹出,因此该能力主要用于横向与纵向菜单场景(官方示例即为mode="horizontal")。- 展开行为本身仍受 Menu 常规属性控制,例如
triggerSubMenuAction(hover/click,默认hover)与延时参数subMenuOpenDelay(默认0秒)/subMenuCloseDelay(默认0.1秒),详见 Menu API 文档。这些参数决定自定义面板何时"呼出"与"收起"。 - 若需微调浮层位置与类名而不重写内容,SubMenu 还提供
popupClassName、popupOffset等属性(对mode="inline"无效);它们与popupRender并不互斥,可配合使用。
完整可运行的参考实现
把上述要点串起来,一个可复制的最小实现(与官方演示逻辑一致)如下:
import React from 'react';
import type { MenuProps } from 'antd';
import { Col, ConfigProvider, Flex, Menu, Row, Space, Typography } from 'antd';
import { createStyles } from 'antd-style';
const { Title, Paragraph } = Typography;
const useStyles = createStyles(({ token }) => ({
navigationPopup: {
padding: token.padding,
minWidth: 480,
background: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary,
},
menuItem: {
borderRadius: token.borderRadius,
transition: `all ${token.motionDurationSlow}`,
cursor: 'pointer',
'&:hover': { background: 'rgba(0, 0, 0, 0.02)' },
},
}));
const MenuItem = ({ title, description }: { title: string; description: string }) => (
<Space direction="vertical" size={4} style={{ padding: 12, width: '100%' }}>
<Title level={5} style={{ margin: 0 }}>{title}</Title>
<Paragraph type="secondary" style={{ margin: 0 }}>{description}</Paragraph>
</Space>
);
const menuItems = [
{ key: 'home', label: 'Home' },
{
key: 'features',
label: 'Features',
children: [
{ key: 'getting-started', label: <MenuItem title="Getting Started" description="Quick start guide." /> },
{ key: 'components', label: <MenuItem title="Components" description="Explore component library." /> },
{ key: 'templates', label: <MenuItem title="Templates" description="Ready-to-use templates." /> },
],
},
{
key: 'resources',
label: 'Resources',
children: [
{ key: 'blog', label: <MenuItem title="Blog" description="Latest updates." /> },
{ key: 'community', label: <MenuItem title="Community" description="Join our community." /> },
],
},
];
const App = () => {
const { styles } = useStyles();
const popupRender: MenuProps['popupRender'] = (_, { item }) => (
<Flex className={styles.navigationPopup} vertical gap="medium">
<Title level={3} style={{ margin: 0, paddingBottom: 8, borderBottom: '1px solid #f0f0f0' }}>
{item.title}
</Title>
<Row gutter={16}>
{React.Children.map(item.children as React.ReactNode, (child) =>
React.isValidElement(child) ? (
<Col span={12} key={child.key}>{child}</Col>
) : null,
)}
</Row>
</Flex>
);
return (
<Menu mode="horizontal" items={menuItems} popupRender={popupRender} />
);
};
export default App;
使用建议与注意事项
- 接管即全量替换:
popupRender的返回值会替代底层默认弹出列表。若只想微调个别样式,优先使用 SubMenu 的popupClassName/popupStyle或组件 Token;只有需要改变弹出内容的整体结构与布局时才使用它。 - 保持面板视觉一致性:自定义面板内建议复用 antd 的浮层类 Token(如
colorBgElevated、boxShadowSecondary),这样在用户切换暗色主题或自定义主题算法时面板不会"穿帮"。 - 善用
props.keys:对于两级以上的嵌套子菜单,可通过 key 路径区分不同层级的展开面板,实现"不同层级不同渲染"。 - 版本前提:本文示例基于当前仓库的 antd 源码(版本见 package.json 的
version字段,即 v6.x 分支),底层依赖@rc-component/menu ~1.5.0;在不同大版本下请以对应版本文档中popupRender的 API 表格为准。
小结
popupRender 让 Menu 摆脱了"只能是纵向文本列表"的形态限制:结合 antd-style 的 Token 样式、ConfigProvider 的组件级主题与 Row/Col/Flex 栅格排版,即可在 Menu 之上快速构建官网级的多列图文导航。若需要研究其完整类型定义与全部弹层相关属性,可在仓库中继续阅读 Menu API(中文)、Menu API(英文) 以及官方 custom-popup-render 演示源码。
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