Ant Design Menu 语义化结构定制实战:classNames 与 styles 的对象/函数用法全解析
导读
在 Ant Design v6 中,Menu 提供了 classNames 与 styles 两个高阶定制入口,允许开发者通过对象或函数两种形态,精准控制菜单内部各语义化结构(Semantic DOM)节点的 class 与行内样式。本文以仓库中 style-class 示例 为主体,结合 Menu API 文档 与 Semantic DOM 演示,完整拆解 Menu 语义化结构的键名体系、两种传参形态的写法差异,以及"对象做静态定制、函数做条件化定制"的实战套路。
一、先理解目标:Menu 的 Semantic DOM 结构
classNames / styles 定制的对象并非随意命名的 class,而是 Menu 组件内部预定义的语义化结构节点。仓库中用于文档演示的 _semantic.tsx 完整列出了这些键名及其含义(中文描述取自该文件 locales.cn):
| 语义键 | 对应节点与职责 |
|---|---|
root |
根元素,包含菜单容器的基础样式和布局 |
item |
条目元素,包含相对定位、块级显示、外边距、空白符处理、光标样式、过渡动画等菜单项基础交互样式 |
itemContent |
条目内容元素,包含菜单项内容的布局与排版 |
itemIcon |
图标元素,包含最小宽度、字体大小、过渡动画、图标重置样式以及与文本的间距控制 |
itemTitle |
菜单标题元素(horizontal 模式不生效),包含标题文字的样式与布局 |
list |
菜单列表元素(horizontal 模式不生效),包含列表的布局与容器样式 |
popup |
弹出菜单(inline 模式不生效),包含弹出层的定位、层级、背景等样式 |
subMenu.itemTitle |
子菜单标题元素 |
subMenu.list |
子菜单列表元素 |
subMenu.item |
子菜单单项元素 |
subMenu.itemIcon |
子菜单条目图标元素 |
subMenu.itemContent |
子菜单条目内容元素 |
从键名可以看出两个要点:
- 支持嵌套写法:形如
subMenu.itemTitle的点分键,说明语义结构可以深入到子菜单内部,而不是只能改外层容器; - 受模式约束:
horizontal(水平)与inline(内嵌)两种 mode 下,部分节点并不渲染——例如list、itemTitle只在非horizontal模式存在,而popup只在非inline模式出现。这解释了_semantic.tsx中mode !== 'inline'/mode !== 'horizontal'的动态键名拼接逻辑。
二、API 形态:对象 or 函数
依据 Menu API 文档,两个 API 的类型签名相同,只是键值类型不同:
| 参数 | 说明 | 类型 |
|---|---|---|
classNames |
用于自定义组件内部各语义化结构的 class | Record<SemanticDOM, string> | (info: { props }) => Record<SemanticDOM, string> |
styles |
用于自定义组件内部各语义化结构的行内 style | Record<SemanticDOM, CSSProperties> | (info: { props }) => Record<SemanticDOM, CSSProperties> |
两者都支持两种传参形态:
- 对象形态:
Record<语义键, 样式值>,静态地给每个结构节点写死样式,适合写法固定的定制需求; - 函数形态:
(info: { props }) => Record<语义键, 样式值>,在渲染时接收 Menu 当前收到的props,据此条件化产出样式表,适合同一份 Menu 在不同场景下要切换样式的需求。
classNames 的值是 class 字符串,styles 的值是 CSSProperties 对象——前者利于复用全局样式文件中的类名,后者适合直接内联书写、无需维护额外样式文件。在仓库 demo/style-class.tsx 的 API 注释与文档的代码演示标记中,这一能力对应版本为 6.0.0(<code src="./demo/style-class.tsx" version="6.0.0">),即使用前请确认组件版本 ≥ 6.0.0。
三、对象形态:classNames + styles 静态定制
style-class.tsx 的 App 中第一个 Menu 组合了三种对象式写法,是静态定制的标准示范:
import React from 'react';
import { Flex, Menu } from 'antd';
import type { GetProp, MenuProps } from 'antd';
import { createStaticStyles } from 'antd-style';
const classNames = createStaticStyles(({ css }) => ({
root: css`
border: 1px solid #f0f0f0;
max-width: 600px;
padding: 8px;
border-radius: 4px;
`,
item: css`
color: #1677ff;
`,
}));
const styles: MenuProps['styles'] = {
root: { border: '1px solid #f0f0f0', padding: 8, borderRadius: 4 },
item: { color: '#1677ff' },
subMenu: { list: { color: '#fa541c' } },
};
3.1 classNames 的用法细节
classNames 对象中直接以语义键命名:root 用于给整个菜单容器加边框、圆角与最大宽度;item 用于统一把菜单项文字改为品牌蓝 #1677ff。
这里用到的 createStaticStyles 来自 antd-style 库(示例中直接 import { createStaticStyles } from 'antd-style'),其回调内接收 css 模板函数,返回以语义键为 key 的静态 CSS 类名集合。这样做的好处是:
- 样式以 CSS 语法书写(含嵌套、伪类、媒体查询),表达力强于行内对象;
- 类名在模块层面静态生成、全局唯一,不会随组件重复实例化而膨胀。
若项目不便引入 antd-style,直接传入普通字符串类名同样成立:
const classNames = {
root: 'my-menu-root',
item: 'my-menu-item',
};
// 配合全局样式表:
// .my-menu-root { border: 1px solid #f0f0f0; border-radius: 4px; }
3.2 styles 的嵌套键写法
与 classNames 不同,styles 除了顶层语义键(root、item),还支持对象嵌套形式来表达点分键,例如示例中的:
subMenu: { list: { color: '#fa541c' } },
等价于给 subMenu.list(子菜单的列表容器)写入 color: '#fa541c'。当结构层级较多、希望按树形组织时,这种嵌套对象比 'subMenu.list': { ... } 的扁平点分键可读性更强——二者在语义键层面指向同一个节点。示例同时也展示了 root / item 键可直接用 CSSProperties 内联声明,非常适合做容器描边、间距、颜色这类轻量视觉微调。
四、函数形态:依据 props 动态产出样式
静态对象无法感知 Menu 实例的差异。函数形态解决的就是这个问题——渲染时拿到 Menu 当前的 props,再决定返回哪些样式:
const stylesFn: MenuProps['styles'] = (info): GetProp<MenuProps, 'styles', 'Return'> => {
const hasSub = info.props.items?.[0];
return {
root: {
backgroundColor: hasSub ? 'rgba(240,249,255, 0.6)' : 'rgba(255,255,255)',
},
};
};
回调签名的入参是 { props },其中 props 即当前 Menu 组件收到的属性集合。上述逻辑先读取 info.props.items 的首项,判断其是否携带 children(即是否包含子菜单结构),据此为 root 选择浅蓝或纯白的背景。
在示例 App 中,函数形态被挂在 mode="inline" 的第二个 Menu 上:
const App: React.FC = () => {
const shareProps: MenuProps = {
classNames,
items,
};
return (
<Flex vertical gap="medium">
<Menu {...shareProps} styles={styles} />
<Menu mode="inline" {...shareProps} styles={stylesFn} />
</Flex>
);
};
注意两个 Menu 共享同一份 items,因此 hasSub 在两个实例中计算结果一致——这里的函数形态展示的是机制本身:一旦多个 Menu 实例的数据源(或 mode、selectedKeys 等 props)不同,函数就能各自返回差异化样式,例如"无子菜单的扁平导航背景加白、有分组/子菜单的导航使用品牌色浅底"。若所有实例配置完全相同,函数与静态对象并无差别,择优使用简单形态即可。
对应的数据类型同样需要精确标注,示例中借助 GetProp<MenuProps, 'styles', 'Return'> 从 MenuProps['styles'] 反推函数返回值类型,保证键名与 CSSProperties 均受类型约束,避免手写 Record 导致拼错语义键。
五、对象与函数的选型建议与边界提醒
综合仓库示例与 API 文档,可归纳以下实践结论:
- 能静态则静态,需分支再上函数:固定的描边、内边距、圆角、统一文字色,直接对象写法;只有样式依赖运行时 props(如是否含子菜单、当前 mode)时,才把
styles/classNames写成函数。 - classNames 与 styles 可叠加使用:示例中同一 Menu 同时传入
classNames与styles(对象),二者分别落为 class 与内联样式,互不覆盖、共同生效。class 适合承载整套视觉体系,inline style 适合承载单点微调。 - 注意 mode 相关的空节点:
horizontal模式下不存在list、itemTitle;inline模式下不存在popup。对不存在的节点写入样式不会报错但也不会产生视觉影响,因此定制前应结合目标 mode 挑选有效语义键。 - 函数形态的类型保持:以函数形式声明时,建议像示例一样显式标注返回值类型,避免弱类型对象导致 TS 丢失语义键与 CSSProperties 的联合校验。
- 版本前提:该定制能力在仓库文档标注为 6.0.0(demo 亦标记
version="6.0.0"),低于此版本请退回传统className/style+ 全局样式覆盖方案。
六、延伸:去文档与源码中继续深挖
- 完整可运行示例见 components/menu/demo/style-class.tsx,其中
classNames、styles、stylesFn三种形态集中在同一文件,适合对照阅读; - 语义结构键名的交互式可视化(含模式切换 Segmented)见 components/menu/demo/_semantic.tsx,中文注释可直接对应到上文键名表;
- API 类型签名与版本说明见 components/menu/index.zh-CN.md(英文版对应 index.en-US.md),其中
ItemType、MenuItemType等数据结构会决定items里哪些字段可被函数形态读取; - 若关心语义化结构 API 在更多组件上的通用约定,可进一步查看仓库其他组件的
demo/*-class.tsx类示例与各自文档的 "Semantic DOM" 小节,用法与 Menu 一致:对象/函数 + 预定义语义键。
整体而言,Menu 的 classNames / styles 把"精细定制组件内部节点"从"依赖内部 DOM 结构的脆弱选择器"升级为"稳定、声明式、类型安全"的公共 API。掌握对象与函数两种形态,即可在保持组件内部实现可升级的前提下,自由控制导航菜单每一层结构的视觉表现。
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