Ant Design Menu 主题切换实战:内置 light / dark 双主题与源码级配色机制解析
导读
Menu 是 Ant Design 中最常被用于搭建导航骨架的组件之一,而它的 theme 属性让开发者可以仅凭一个字符串就完成整棵菜单树从浅色到深色的视觉切换。本篇围绕 components/menu/demo/theme.md 所述内容展开:先给出可直接运行的双主题切换示例,再进入源码确认 theme 参数的默认值、类型定义与 className 生成逻辑,最后拆解 dark 主题背后成套的组件级 Design Token,帮助你既能在业务中熟练切换主题,也能理解深浅色视觉差异的底层来源。
主题能力概述:两套内置主题,默认 light
在 Menu 的官方示例描述中明确说明了这一事实:
Menu 内建了两套主题
light和dark,默认light。(There are two built-in themes:lightanddark. The default value islight.)
这句话对应到 API 上就是 Menu 组件暴露的 theme 属性,其取值范围被严格约束为两个枚举值之一。在 Menu API 文档 中,该属性的定义为:
| Property | Description | Type | Default |
|---|---|---|---|
| theme | 菜单主题颜色 | light | dark |
light |
类型层面,MenuTheme 联合类型定义在 components/menu/MenuContext.tsx:
export type MenuTheme = 'light' | 'dark';
它同时通过 components/menu/index.tsx 从组件包的入口重新导出,因此业务代码可以直接 import type { MenuTheme } from 'antd' 使用。这意味着一套菜单只会在两种固定主题之间切换,杜绝了自由字符串带来的样式错乱,也方便类型系统在编译期帮你捕获拼写错误。
可直接运行的完整示例:Switch 切换 dark / light
配套的演示代码 components/menu/demo/theme.tsx 给出了一个完整、可直接复制的实现:用 Ant Design 的 Switch 组件作为主题切换开关,选中态即为 dark,取消选中回到 light,同时配合受控的 selectedKeys 展示菜单选中效果。
import React, { useState } from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps, MenuTheme } from 'antd';
import { Menu, Switch } from 'antd';
type MenuItem = Required<MenuProps>['items'][number];
const items: MenuItem[] = [
{
key: 'sub1',
label: 'Navigation One',
icon: <MailOutlined />,
children: [
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' },
{ key: '3', label: 'Option 3' },
{ key: '4', label: 'Option 4' },
],
},
{
key: 'sub2',
label: 'Navigation Two',
icon: <AppstoreOutlined />,
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 App: React.FC = () => {
const [theme, setTheme] = useState<MenuTheme>('dark');
const [current, setCurrent] = useState('1');
const changeTheme = (value: boolean) => {
setTheme(value ? 'dark' : 'light');
};
const onClick: MenuProps['onClick'] = (e) => {
console.log('click ', e);
setCurrent(e.key);
};
return (
<>
<Switch
checked={theme === 'dark'}
onChange={changeTheme}
checkedChildren="Dark"
unCheckedChildren="Light"
/>
<br />
<br />
<Menu
theme={theme}
onClick={onClick}
style={{ width: 256 }}
defaultOpenKeys={['sub1']}
selectedKeys={[current]}
mode="inline"
items={items}
/>
</>
);
};
export default App;
该示例的关键手法可以拆成三点复用到实际业务中:
- 用布尔状态桥接双主题:
Switch的checked天然对应布尔值,而MenuTheme是二选一字符串,因此通过theme === 'dark'与changeTheme中的三元表达式完成互转,状态单一、逻辑直观。 items与渲染分离:菜单数据结构完全由items描述(含多级子菜单sub3),组件内部不再写 JSX 子节点,便于主题切换时仅重渲染样式而非重建结构。- 受控选中与受控主题并行:
selectedKeys={[current]}让选中态由 React 状态驱动,onClick中更新current,这样切换主题后当前高亮项不会丢失。
源码解析:theme 默认值、上下文与 className 的生成
在 components/menu/menu.tsx 中,MenuProps 显式声明了 theme?: MenuTheme,而组件实现内部通过解构默认值将其兜底为 light(见 menu.tsx 第 141 行):
theme = 'light',
这与文档中「默认 light」的描述完全一致——即便调用方完全不传 theme,组件也会以 light 主题渲染。顺着渲染流程可以看到 theme 的两种下游去向:
去向一:生成根节点与溢出弹出层的主题 class。 组件组合出形如 ${prefixCls}-${theme} 的类名(menu.tsx 第 244 行),在默认 prefixCls 为 ant-menu 时即得到 ant-menu-light / ant-menu-dark;当菜单内容溢出、以弹出层形式展示时(横向模式的 overflow 场景),弹出层同样会拼接该主题 class(menu.tsx 第 302-306 行),保证子菜单弹出面板与父级主题一致。
去向二:写入 MenuContext 供后代消费。 在 menu.tsx 第 269-293 行 构造的 MenuContextProps 中,theme 被放入 context。而 components/menu/MenuContext.tsx 的 theme?: MenuTheme 字段说明:菜单内部的 SubMenu、MenuItem 等子结构并不各自持有主题副本,而是统一从祖先 context 读取,这正是「整棵树一键换肤」的机制基础——你只需在外层 Menu 上传一次 theme,所有层级的展开箭头、悬停态、选中态都会联动变化。
dark 主题的底层视觉来源:成套的 dark 组件 Token
主题切换的视觉效果并不来自硬编码的随机颜色,而是由一组以 dark* 命名的组件级 Token 驱动,定义在 components/menu/style/index.ts 的 MenuToken 中(覆盖背景、文字、悬停、选中、危险项、分组标题、弹出面板等维度的 dark 态取值)。其默认值通过 getDefaultToken 汇总,例如:
darkItemBg: '#001529'(深色菜单底色)darkPopupBg: '#001529'(深色弹出子菜单底色)darkSubMenuItemBg: '#000c17'(深色子菜单项的背景,比菜单底色更暗一层,用于体现层级)darkItemSelectedBg: colorPrimary(选中项直接使用主题主色高亮)darkItemColor: colorTextDark、darkItemHoverColor: colorTextLightSolid等文字颜色- 禁用态
darkItemDisabledColor通过new FastColor(colorTextLightSolid).setA(0.25).toRgbString()在亮色文字基础上降低透明度生成(见 style/index.ts)
也就是说,dark 主题的「深色底 + 主色选中块 + 亮色文字」观感,是这些 Token 协同作用的结果。样式生成侧则通过 theme/internal 的 genStyleHooks、mergeToken 等工具将 Token 编译为最终 CSS-in-JS 样式,并依据类名 ant-menu-dark 生效,相关结构可参考 components/menu/style/index.ts 与同目录下的 theme.ts(getThemeStyle)。
若你需要在主题色、圆角、暗色底等维度做企业级定制,正确的姿势是使用 ConfigProvider 的主题 Token 能力,而不是直接覆盖这几个 dark* 字符串——这样既保留内置主题的联动逻辑,又能让视觉与整体设计语言保持一致。
周边联动场景
围绕内置双主题,仓库还提供了多个可对照参考的相邻演示,帮助你覆盖更多布局形态:
- 深色顶部导航:components/menu/demo/horizontal-dark.tsx(说明见 horizontal-dark.md)展示
theme="dark"配合mode="horizontal"的水平顶部导航形态,常用于站点级顶栏。 - 子菜单独立换肤:components/menu/demo/submenu-theme.tsx 演示在保持父级 Menu 主题的同时,通过
theme单独设置弹出子菜单的明暗。对应 API 上 SubMenu 也拥有theme?: 'light' | 'dark'属性,且默认继承自外层 Menu(详见 Menu API 文档)。 - 模式与主题组合切换:components/menu/demo/switch-mode.tsx 通过
GetProp<MenuProps, 'theme'>提取主题类型,同时演示mode与theme的自由组合,是布局切换类后台系统的常用模板。
小结
Menu 的 light / dark 双主题看似只是一个布尔开关,但背后串联了「类型定义(MenuTheme)→ 组件默认值(light)→ context 传递 → 主题 class 生成 → dark Token 着色」的完整链路。在业务中使用时,你只需记住三点:theme 只接受 'light' | 'dark' 两个值且默认 light;通过受控状态加 Switch 即可实现运行时切换;若对深色观感不满意,请通过 ConfigProvider 主题 Token 定制而非直接覆写样式类。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
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