Material-UI Tabs 组件完全指南:从基础用法到滚动、导航与无障碍的源码级解析
MUI(Material UI)的 Tabs 组件用于在多个同层级视图之间组织与切换内容。本篇以官方文档 Tabs 为主体,完整覆盖组件族构成、@mui/lab 实验 API、面板常驻挂载、滚动按钮策略、固定/全宽/垂直布局、图标标签、路由集成与无障碍要求,并结合 packages/mui-material 源码解释默认值、槽位(slots)体系与滚动指示器的底层实现,帮助你从使用到调优全面掌握 Tabs。
1. Tabs 组件族:四个组件如何协作
官方文档将 Tabs 定义为“让内容在不同视图之间组织与切换”的组件,它由一组协作组件实现:
| 组件 | 导入来源 | 职责 |
|---|---|---|
<Tabs /> |
@mui/material/Tabs |
标签容器,负责焦点管理、键盘导航、指示器定位与滚动 |
<Tab /> |
@mui/material/Tab |
单个标签项,点击后切换到对应面板 |
<TabScrollButton /> |
@mui/material/TabScrollButton |
可滚动标签栏两侧的滚动按钮(内部使用,可通过槽位替换) |
<TabContext /> / <TabList /> / <TabPanel /> |
@mui/lab |
实验 API:自动注入 ARIA 属性的顶层上下文、标签列表、内容面板 |
从源码结构看,Tabs 的对外出口在 index.js,默认导出 Tabs 组件并导出 tabsClasses;核心的样式类名由 tabsClasses.ts 生成(root、vertical、list、centered、scroller、fixed、scrollableX、scrollableY、hideScrollbar、scrollButtons、scrollButtonsHideMobile、indicator),这些类名既可用于 classes 覆盖,也是 CSS 覆盖选择器(如 .MuiTabs-scrollButtons)的依据。
2. 基础用法
标准 API 的安装导入方式:
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
完整的基础示例(对应仓库中的 BasicTabs.tsx)展示了受控模式:value 保存当前选中标签,onChange 更新状态,并手工完成 ARIA 关联。
import * as React from 'react';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
tabIndex={0}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
export default function BasicTabs() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
</Box>
<CustomTabPanel value={value} index={0}>Item One</CustomTabPanel>
<CustomTabPanel value={value} index={1}>Item Two</CustomTabPanel>
<CustomTabPanel value={value} index={2}>Item Three</CustomTabPanel>
</Box>
);
}
几个关键行为可以从源码得到解释:
value的取值映射:Tabs.js 在渲染时遍历children,若<Tab>未显式提供value,则以子元素索引作为其值(childValue = child.props.value === undefined ? index : child.props.value),并通过valueToIndexMap 建立“值 → 下标”的映射;若value与任何子项都匹配不上,开发环境会打印警告并列出所有合法取值。将value设为false表示“当前没有选中标签”。- 指示器(indicator)的定位:组件在每次布局变化时读取选中标签与滚动容器的
getBoundingClientRect()计算left/top与width/height,只有位移或尺寸变化 ≥ 1px 才触发重渲染;指示器基础样式为height: 2、bottom: 0、width: 100%,并带有主题过渡动画(见 Tabs.js 中TabsIndicator的定义)。 - 首次渲染不闪动:指示器在挂载前被注入到选中的
<Tab>内部(indicator: selected && !mounted && indicator),挂载后才作为独立绝对定位元素渲染,避免初始位置动画。
3. 实验 API:@mui/lab 的 TabContext / TabList / TabPanel
文档指出,@mui/lab 提供一组自动注入 ARIA 属性的工具组件,省去手动编写 id / aria-controls 的工作:
<TabList />—— 标签容器,负责焦点与键盘导航;<TabPanel />—— 承载某个标签对应内容的面板;<TabContext />—— 包裹TabList与TabPanel的顶层上下文,负责分发当前value。
对应示例(LabTabs.tsx):
import * as React from 'react';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import TabContext from '@mui/lab/TabContext';
import TabList from '@mui/lab/TabList';
import TabPanel from '@mui/lab/TabPanel';
export default function LabTabs() {
const [value, setValue] = React.useState('1');
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box sx={{ width: '100%', typography: 'body1' }}>
<TabContext value={value}>
<TabList onChange={handleChange} aria-label="lab tabs" sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tab label="Item One" value="1" />
<Tab label="Item Two" value="2" />
</TabList>
<TabPanel value="1" tabIndex={0}>Item One</TabPanel>
<TabPanel value="2" tabIndex={0}>Item Two</TabPanel>
</TabContext>
</Box>
);
}
在仓库中,这组组件实现位于 packages/mui-lab/src/TabContext、packages/mui-lab/src/TabList、packages/mui-lab/src/TabPanel 目录(例如 TabContext.d.ts),它们基于 @mui/material 的 Tabs 封装,通过 Context 将 value 传递给各 TabPanel,自动完成面板与标签的 aria 关联。注意该 API 被官方标记为 Experimental,语义上承诺不如稳定 API 强,生产项目选型时建议自行评估。
4. 让面板始终保持挂载(keepMounted)
文档明确给出策略建议:保持面板挂载可以保留组件状态并让后续切换更快;但所有面板会在初始加载时全部渲染、占用更多内存,且隐藏面板中的副作用仍可能继续执行。因此只对有状态或高频访问的面板选择性启用。
4.1 使用 lab API
给每个 TabPanel 传 keepMounted(示例见 KeepMountedLabTabs.tsx):
<TabPanel value="1" keepMounted>Item One</TabPanel>
<TabPanel value="2" keepMounted>Item Two</TabPanel>
4.2 使用标准 API
标准 API 下,面板子元素无条件渲染,用 hidden 属性控制可见性——这正是上文 CustomTabPanel 中 hidden={value !== index} 的写法:始终挂载、靠 hidden 隐藏,天然保持状态;若把内容条件渲染为 {value === index && ...},则切换后状态会丢失。
5. 标签样式与状态:换行、着色、禁用
5.1 长标签自动换行
文档说明:长标签会自动换行显示;若标签过长超出单个 tab 的宽度,文字会溢出且不可见。对应示例为 TabsWrappedLabel.tsx。这提示在使用长文案时应控制标签长度,或结合自定义样式压缩间距。
5.2 彩色标签
通过 indicatorColor 与 textColor 控制指示器与文字颜色。源码中二者的默认值均为 'primary'(Tabs.js 的解构默认值):
indicatorColor取'primary' | 'secondary'或任意 CSS 颜色字符串,指示器背景色取自theme.palette.<color>.main;textColor取'inherit' | 'primary' | 'secondary',透传给每个子<Tab>。
示例:
<Tabs indicatorColor="secondary" textColor="secondary" value={value} onChange={handleChange} />
5.3 禁用标签
设置 disabled 即可禁用某个标签,使其不可点击、不可聚焦(示例 DisabledTabs.tsx)。
6. 固定标签:fullWidth 与 centered
文档建议“固定标签适用于标签数量有限、且一致的布局有助于肌肉记忆”的场景,分两档:
variant="fullWidth"(全宽):用于小屏幕,让所有标签平分可用宽度;centered(居中):用于大屏,标签整体居中而不拉伸。
// 全宽(示例 FullWidthTabs.tsx)
<Tabs value={value} onChange={handleChange} variant="fullWidth" />
// 居中(示例 CenteredTabs.tsx)
<Tabs value={value} onChange={handleChange} centered />
源码层面的佐证:
variant在内部派生出fixed: !scrollable的 ownerState;fullWidth会把fullWidth: true克隆注入每个子<Tab>,使每个 tab 占据等分空间;centered只有在非 scrollable 时才生效(centered: centered && !scrollable),List 槽位据此应用justifyContent: 'center';- 二者不能同时使用:Tabs.js 在开发环境下会打印 “You can not use the
centered={true}andvariant="scrollable"properties at the same time” 的错误。
7. 可滚动标签:scrollButtons 的四种策略
当标签数量超出容器时,使用 variant="scrollable" 启用滚动。滚动按钮的行为由 scrollButtons 与 allowScrollButtonsMobile 共同决定,文档给出了四种组合:
| 组合 | 效果 |
|---|---|
variant="scrollable" + scrollButtons="auto"(默认) |
桌面端在标签溢出时显示左右滚动按钮,移动端隐藏 |
scrollButtons={true} + allowScrollButtonsMobile |
所有视口尺寸都强制显示左右滚动按钮 |
| 自定义不透明度的滚动按钮 | 配合 CSS 让按钮“始终可见”(只是禁用态变淡) |
scrollButtons={false} |
永不显示滚动按钮,滚动完全交给用户代理机制(左右滑动手势、Shift+滚轮等) |
对应示例分别位于 ScrollableTabsButtonAuto.tsx、ScrollableTabsButtonForce.tsx、ScrollableTabsButtonVisible.tsx、ScrollableTabsButtonPrevent.tsx。
想让按钮“始终可见”时,需要覆盖禁用态的不透明度(默认禁用态几乎不可见):
.MuiTabs-scrollButtons.Mui-disabled {
opacity: 0.3;
}
源码中的实现细节(均在 Tabs.js):
- 按钮何时出现:
showScrollButtons = scrollable && ((scrollButtons === 'auto' && scrollButtonsActive) || scrollButtons === true);scrollButtonsActive由首尾两个标签的可见性状态决定。 - 可见性检测:组件用两个
IntersectionObserver(root为滚动容器、threshold: 0.99)分别观察第一个与最后一个标签,一旦首/尾标签未完全进入视口就点亮对应方向的按钮(Tabs.js)。 - 点击滚动距离:
getScrollSize()累加标签尺寸直到超出容器,若第一个标签本身已超宽则只滚动一个容器宽度,再通过animate做带主题时长的滚动动画(reducedMotion开启时直接赋值scrollLeft/top)。 - 移动端隐藏:未开启
allowScrollButtonsMobile时,根节点应用scrollButtonsHideMobile变体,在breakpoints.down('sm')下将按钮display: none——这就是“桌面显示、移动隐藏”的默认行为来源。 - 滚动条:scrollable 模式下默认隐藏原生滚动条(
hideScrollbar类 + 负margin补偿),组件内部渲染一个不可见的ScrollbarSize测量组件(ScrollbarSize.js)来补偿被隐藏滚动条占据的空间,避免与内容重叠。
8. 垂直标签与 visibleScrollbar
将 orientation="vertical" 即可切换为垂直排布(示例 VerticalTabs.tsx)。内部推导:
const vertical = orientation === 'vertical';
// ownerState 中:
scrollableX: scrollable && !vertical,
scrollableY: scrollable && vertical,
hideScrollbar: scrollable && !visibleScrollbar,
也就是说垂直 + variant="scrollable" 时溢出方向为 overflowY: auto;文档同时提醒:垂直滚动条默认被隐藏,可通过 visibleScrollbar={true} 恢复滚动条显示(PropTypes 注释说明其对“很长的垂直标签列表”特别有用)。视觉上,垂直模式下指示器变为 height: 100%、width: 2、right: 0,贴合标签栏右缘。
9. 导航型标签(Nav Tabs)与路由集成
默认每个 <Tab> 渲染为 button 元素;文档指出可以通过自定义标签/组件把 Tabs 用作页内导航(示例 NavTabs.tsx)。
官方文档“第三方路由库”一节的核心做法是:利用 Tab 的 component 属性把每个标签替换为路由 Link,从而实现纯客户端跳转、无 HTTP 往返的标签导航:
import { Link as RouterLink } from 'react-router-dom';
<Tab
component={RouterLink}
to="/about"
label="About"
value="/about"
/>
此时 value 通常与路由路径保持一致,Tabs 的受控 value 由当前路由决定(通常结合 useLocation),点击链接后由路由库更新 URL,再反向驱动 value。仓库文档在 routing 集成指南 中提供了更完整的讲解。
10. 图标标签与图标位置
文档规定:标签栏内要么全部使用图标,要么全部使用文本,两种示例分别对应 IconTabs.tsx 与 IconLabelTabs.tsx:
<Tab icon={<HomeIcon />} label="Home" /> // 图标 + 文本
<Tab icon={<HomeIcon />} aria-label="Home" /> // 纯图标时建议提供 aria-label
图标位置由 icon 布局控制:默认图标位于标签顶部(top),也支持 start、end、bottom 三种位置(示例 IconPositionTabs.tsx)。
11. 无障碍(Accessibility)
文档依据 WAI-ARIA Tabs 模式(frontmatter 中 waiAria 字段即指向该规范),列出标准 API 下手工满足无障碍的三步要求:
- 用
aria-label或aria-labelledby为Tabs整体提供标签(源码中二者被直接透传到role="tablist"的 List 槽位上,见 Tabs.js); - 每个
<Tab>必须与其[role="tabpanel"]关联:通过id、aria-controls、aria-labelledby三者配对(即第 2 节示例中a11yProps的作用); - 若面板内没有可聚焦内容、或第一个有意义的内容元素不可聚焦,则在面板上设置
tabIndex={0}。
如果不想手工维护这些属性,可改用 @mui/lab 的实验 API(第 3 节),它会完成同样的关联工作。
键盘导航:手动激活 vs 焦点跟随选择
组件默认实现 WAI-ARIA 的 “manual activation”(手动激活) 行为:方向键只移动焦点,需要回车/空格才切换标签。若希望 “选择自动跟随焦点”,给 Tabs 传 selectionFollowsFocus:
<Tabs selectionFollowsFocus /> {/* 焦点移动即切换 */}
<Tabs /> {/* 需要手动激活 */}
两个对照示例为 AccessibleTabs1.tsx 与 AccessibleTabs2.tsx。从源码结构看,键盘导航由内部的 roving tabindex 机制(useRovingTabIndexRoot)驱动:List 上监听 onKeyDown,仅在活动元素 role="tab" 时接管方向键处理,并通过 RovingTabIndexContext 把容器行为下发给每个 <Tab>;selectionFollowsFocus 则被克隆注入到每个子 <Tab>,决定“聚焦即选中”与否(Tabs.js)。
12. 定制:classes、slots 与 slotProps
文档的 Customization 章节(示例 CustomizedTabs.tsx)展示了通过主题 components.MuiTabs.styleOverrides 改写默认样式的方式。结合源码,Tabs 提供三种定制入口:
classes/ CSS 类名:tabsClasses暴露的 12 个类名(如.MuiTabs-indicator、.MuiTabs-scrollButtons),第 7 节的opacity覆盖即属此类;slots:可替换的组件类型,包括root、scroller、list、indicator、scrollbar、scrollButtons、startScrollButtonIcon、endScrollButtonIcon(对应 Tabs.js 的 PropTypes 中slots形状定义),例如换成自定义的指示器图形;slotProps:为各槽位传递 props 或 props 生成函数,用于在保留默认组件的情况下注入行为或样式。
此外,Tabs 支持 action 属性以编程方式触发 updateIndicator() 与 updateScrollButtons()(实现见 Tabs.js),适用于异步数据导致标签数量变化后手动刷新指示器/滚动按钮的场景。
13. 源码结构与延伸阅读
与本文相关的关键仓库路径:
- Tabs.js:Tabs 核心实现(props 默认值、指示器测量、滚动按钮观察器、键盘导航、slots);
- tabsClasses.ts:类名定义与
getTabsUtilityClass; - ScrollbarSize.js:滚动条宽度测量组件;
- Tab.js:单个标签项实现;
packages/mui-lab/src/TabContext、TabList、TabPanel:实验 API 实现;- tabs.md 与同目录下的 20 余个演示文件:全部官方示例源码(
.js与.tsx双版本)。
阅读建议:先在业务中从第 2 节基础示例起步;需要省掉 ARIA 样板代码时评估 @mui/lab 方案;标签数量多时按第 7 节选择 scrollButtons 策略;对接路由时按第 9 节替换 Tab 的 component;对无障碍有硬性要求时对照第 11 节逐条自检键盘与焦点行为。
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 StartedRust0622
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