首页
/ Ant Design Menu `popupRender` 实战:打造企业级多级导航的"超级下拉面板"

Ant Design Menu `popupRender` 实战:打造企业级多级导航的"超级下拉面板"

2026-09-07 16:55:26作者:胡易黎Nicole

导读

在 Ant Design 中,当菜单项包含子菜单(SubMenu)时,horizontalvertical 等模式下子项默认以一层简单的弹出面板呈现。若你希望把下拉内容做成"图文卡片式超级导航"(如首页顶部常见的巨型下拉、两级分类导航),就需要使用 Menu 的 popupRender 属性整体接管弹出面板的渲染。本文将基于当前仓库的官方演示与源码,讲解 popupRender 的签名与触发时机、它在 Menu 与 SubMenu 两个层级的配置方式,并结合一份可运行的完整代码,演示如何用 Flex / Row / Col、antd-style 与 ConfigProvider 组合出多列内容的下拉面板。

从官方示例说起:custom-popup-render 想要表达什么

Menu 官方演示目录 中,页面使用了一个 mode="horizontal" 的水平菜单,菜单数据由两个带 children 的子菜单(featuresresources)和一个普通项(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)配置数据,可读取其 titlechildrenkey 等字段
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.tsSubMenuType 的定义),因此示例中先将其视为 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)与带 zIndexpopupStyle 后,item 级 popupRender 会随展开逻辑一并被底层使用。因此可以推断:运行时以"最靠近当前子菜单"的配置为准,item 级覆盖会优先于 Menu 级默认值

使用对象语法时的写法

当菜单数据由 items 提供时,把 popupRender 直接写在某个子菜单节点上即可实现"仅该子菜单使用自定义弹出面板":

const items = [
  {
    key: 'features',
    label: 'Features',
    popupRender: (node, { item }) => <YourPanel item={item} />,
    children: [ /* ... */ ],
  },
];

撑起"超级下拉面板"的样式与主题配套

只替换内容还不够,面板外观需要靠外层样式与主题 Token 一起配合。官方示例做了三件事,值得完整参考。

1. 用 antd-style 设计弹出面板外观

示例通过 createStylesantd-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 常规属性控制,例如 triggerSubMenuActionhover / click,默认 hover)与延时参数 subMenuOpenDelay(默认 0 秒)/ subMenuCloseDelay(默认 0.1 秒),详见 Menu API 文档。这些参数决定自定义面板何时"呼出"与"收起"。
  • 若需微调浮层位置与类名而不重写内容,SubMenu 还提供 popupClassNamepopupOffset 等属性(对 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(如 colorBgElevatedboxShadowSecondary),这样在用户切换暗色主题或自定义主题算法时面板不会"穿帮"。
  • 善用 props.keys:对于两级以上的嵌套子菜单,可通过 key 路径区分不同层级的展开面板,实现"不同层级不同渲染"。
  • 版本前提:本文示例基于当前仓库的 antd 源码(版本见 package.jsonversion 字段,即 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 演示源码

登录后查看全文
热门项目推荐
相关项目推荐