首页
/ Material-UI Tabs 组件完全指南:从基础用法到滚动、导航与无障碍的源码级解析

Material-UI Tabs 组件完全指南:从基础用法到滚动、导航与无障碍的源码级解析

2026-09-04 19:42:42作者:咎竹峻Karen

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 生成(rootverticallistcenteredscrollerfixedscrollableXscrollableYhideScrollbarscrollButtonsscrollButtonsHideMobileindicator),这些类名既可用于 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),并通过 valueToIndex Map 建立“值 → 下标”的映射;若 value 与任何子项都匹配不上,开发环境会打印警告并列出所有合法取值。将 value 设为 false 表示“当前没有选中标签”。
  • 指示器(indicator)的定位:组件在每次布局变化时读取选中标签与滚动容器的 getBoundingClientRect() 计算 left/topwidth/height,只有位移或尺寸变化 ≥ 1px 才触发重渲染;指示器基础样式为 height: 2bottom: 0width: 100%,并带有主题过渡动画(见 Tabs.jsTabsIndicator 的定义)。
  • 首次渲染不闪动:指示器在挂载前被注入到选中的 <Tab> 内部(indicator: selected && !mounted && indicator),挂载后才作为独立绝对定位元素渲染,避免初始位置动画。

3. 实验 API:@mui/lab 的 TabContext / TabList / TabPanel

文档指出,@mui/lab 提供一组自动注入 ARIA 属性的工具组件,省去手动编写 id / aria-controls 的工作:

  • <TabList /> —— 标签容器,负责焦点与键盘导航;
  • <TabPanel /> —— 承载某个标签对应内容的面板;
  • <TabContext /> —— 包裹 TabListTabPanel 的顶层上下文,负责分发当前 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/TabContextpackages/mui-lab/src/TabListpackages/mui-lab/src/TabPanel 目录(例如 TabContext.d.ts),它们基于 @mui/materialTabs 封装,通过 Context 将 value 传递给各 TabPanel,自动完成面板与标签的 aria 关联。注意该 API 被官方标记为 Experimental,语义上承诺不如稳定 API 强,生产项目选型时建议自行评估。

4. 让面板始终保持挂载(keepMounted)

文档明确给出策略建议:保持面板挂载可以保留组件状态并让后续切换更快;但所有面板会在初始加载时全部渲染、占用更多内存,且隐藏面板中的副作用仍可能继续执行。因此只对有状态或高频访问的面板选择性启用

4.1 使用 lab API

给每个 TabPanelkeepMounted(示例见 KeepMountedLabTabs.tsx):

<TabPanel value="1" keepMounted>Item One</TabPanel>
<TabPanel value="2" keepMounted>Item Two</TabPanel>

4.2 使用标准 API

标准 API 下,面板子元素无条件渲染,用 hidden 属性控制可见性——这正是上文 CustomTabPanelhidden={value !== index} 的写法:始终挂载、靠 hidden 隐藏,天然保持状态;若把内容条件渲染为 {value === index && ...},则切换后状态会丢失。

5. 标签样式与状态:换行、着色、禁用

5.1 长标签自动换行

文档说明:长标签会自动换行显示;若标签过长超出单个 tab 的宽度,文字会溢出且不可见。对应示例为 TabsWrappedLabel.tsx。这提示在使用长文案时应控制标签长度,或结合自定义样式压缩间距。

5.2 彩色标签

通过 indicatorColortextColor 控制指示器与文字颜色。源码中二者的默认值均为 '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} and variant="scrollable" properties at the same time” 的错误。

7. 可滚动标签:scrollButtons 的四种策略

当标签数量超出容器时,使用 variant="scrollable" 启用滚动。滚动按钮的行为由 scrollButtonsallowScrollButtonsMobile 共同决定,文档给出了四种组合:

组合 效果
variant="scrollable" + scrollButtons="auto"(默认) 桌面端在标签溢出时显示左右滚动按钮,移动端隐藏
scrollButtons={true} + allowScrollButtonsMobile 所有视口尺寸都强制显示左右滚动按钮
自定义不透明度的滚动按钮 配合 CSS 让按钮“始终可见”(只是禁用态变淡)
scrollButtons={false} 永不显示滚动按钮,滚动完全交给用户代理机制(左右滑动手势、Shift+滚轮等)

对应示例分别位于 ScrollableTabsButtonAuto.tsxScrollableTabsButtonForce.tsxScrollableTabsButtonVisible.tsxScrollableTabsButtonPrevent.tsx

想让按钮“始终可见”时,需要覆盖禁用态的不透明度(默认禁用态几乎不可见):

.MuiTabs-scrollButtons.Mui-disabled {
  opacity: 0.3;
}

源码中的实现细节(均在 Tabs.js):

  • 按钮何时出现showScrollButtons = scrollable && ((scrollButtons === 'auto' && scrollButtonsActive) || scrollButtons === true)scrollButtonsActive 由首尾两个标签的可见性状态决定。
  • 可见性检测:组件用两个 IntersectionObserverroot 为滚动容器、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: 2right: 0,贴合标签栏右缘。

9. 导航型标签(Nav Tabs)与路由集成

默认每个 <Tab> 渲染为 button 元素;文档指出可以通过自定义标签/组件把 Tabs 用作页内导航(示例 NavTabs.tsx)。

官方文档“第三方路由库”一节的核心做法是:利用 Tabcomponent 属性把每个标签替换为路由 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.tsxIconLabelTabs.tsx

<Tab icon={<HomeIcon />} label="Home" />          // 图标 + 文本
<Tab icon={<HomeIcon />} aria-label="Home" />     // 纯图标时建议提供 aria-label

图标位置由 icon 布局控制:默认图标位于标签顶部(top),也支持 startendbottom 三种位置(示例 IconPositionTabs.tsx)。

11. 无障碍(Accessibility)

文档依据 WAI-ARIA Tabs 模式(frontmatter 中 waiAria 字段即指向该规范),列出标准 API 下手工满足无障碍的三步要求:

  1. aria-labelaria-labelledbyTabs 整体提供标签(源码中二者被直接透传到 role="tablist" 的 List 槽位上,见 Tabs.js);
  2. 每个 <Tab> 必须与其 [role="tabpanel"] 关联:通过 idaria-controlsaria-labelledby 三者配对(即第 2 节示例中 a11yProps 的作用);
  3. 若面板内没有可聚焦内容、或第一个有意义的内容元素不可聚焦,则在面板上设置 tabIndex={0}

如果不想手工维护这些属性,可改用 @mui/lab 的实验 API(第 3 节),它会完成同样的关联工作。

键盘导航:手动激活 vs 焦点跟随选择

组件默认实现 WAI-ARIA 的 “manual activation”(手动激活) 行为:方向键只移动焦点,需要回车/空格才切换标签。若希望 “选择自动跟随焦点”,给 TabsselectionFollowsFocus

<Tabs selectionFollowsFocus />  {/* 焦点移动即切换 */}
<Tabs />                        {/* 需要手动激活 */}

两个对照示例为 AccessibleTabs1.tsxAccessibleTabs2.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 提供三种定制入口:

  1. classes / CSS 类名tabsClasses 暴露的 12 个类名(如 .MuiTabs-indicator.MuiTabs-scrollButtons),第 7 节的 opacity 覆盖即属此类;
  2. slots:可替换的组件类型,包括 rootscrollerlistindicatorscrollbarscrollButtonsstartScrollButtonIconendScrollButtonIcon(对应 Tabs.js 的 PropTypes 中 slots 形状定义),例如换成自定义的指示器图形;
  3. 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/TabContextTabListTabPanel:实验 API 实现;
  • tabs.md 与同目录下的 20 余个演示文件:全部官方示例源码(.js.tsx 双版本)。

阅读建议:先在业务中从第 2 节基础示例起步;需要省掉 ARIA 样板代码时评估 @mui/lab 方案;标签数量多时按第 7 节选择 scrollButtons 策略;对接路由时按第 9 节替换 Tabcomponent;对无障碍有硬性要求时对照第 11 节逐条自检键盘与焦点行为。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341