Material UI Breadcrumbs 组件实战:React 面包屑导航的完整用法、折叠机制与源码解析
Breadcrumbs(面包屑导航)是 Material UI 提供的列表式链接组件,用于可视化页面在站点层级结构中的位置,并允许用户快速返回任意上级节点。本篇基于仓库中的官方文档 breadcrumbs.md 与其配套的 8 个演示代码(docs/data/material/components/breadcrumbs/ 目录),并深入到 Breadcrumbs.js 的源码实现,完整讲解基础用法、活跃末项、自定义分隔符、折叠(collapsed)机制、react-router 集成与无障碍要求,帮助你把面包屑直接落到生产项目中。
一、基本用法:Link + Typography 组合
面包屑组件本身不关心子项是链接还是纯文本,它只负责把子项包进 <ol><li> 结构并插入分隔符。官方文档定义其为:"a list of links that help visualize a page's location within a site's hierarchical structure"(breadcrumbs.md)。
最简示例来自 BasicBreadcrumbs.tsx:
import * as React from 'react';
import Typography from '@mui/material/Typography';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import Link from '@mui/material/Link';
function handleClick(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
event.preventDefault();
console.info('You clicked a breadcrumb.');
}
export default function BasicBreadcrumbs() {
return (
<div role="presentation" onClick={handleClick}>
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/">
MUI
</Link>
<Link
underline="hover"
color="inherit"
href="/material-ui/getting-started/installation/"
>
Core
</Link>
<Typography sx={{ color: 'text.primary' }}>Breadcrumbs</Typography>
</Breadcrumbs>
</div>
);
}
几个关键约定值得注意:
- 中间层级用
Link(underline="hover" color="inherit"保持面包屑的次级文字颜色); - 当前页不是链接,而是一段
Typography,并用color: 'text.primary'将其从继承的textSecondary提亮为前景色——这是 Material UI 文档站的统一写法; - 外层
div上的onClick只是演示用的统一事件代理,生产代码中通常直接给每个Link绑定事件或交给路由处理。
二、让最后一级面包屑保持可交互
默认做法是把末项渲染成文本,但有些场景(例如"编辑/新建"共用同一列表页)需要末项仍然是可点击的链接。ActiveLastBreadcrumb.tsx 给出了官方写法:末项使用 Link,并通过 aria-current="page" 明确告知屏幕阅读器"这就是当前页":
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/">
MUI
</Link>
<Link underline="hover" color="inherit" href="/material-ui/getting-started/installation/">
Core
</Link>
<Link
underline="hover"
href="/material-ui/react-breadcrumbs/"
aria-current="page"
sx={{
color: 'text.primary',
}}
>
Breadcrumbs
</Link>
</Breadcrumbs>
与纯文本版本相比只有两处不同:末项由 Typography 换成 Link,并加上 aria-current="page" 与 sx={{ color: 'text.primary' }} 保持视觉上"当前页"的高亮语义。
三、自定义分隔符:字符串与 SVG 图标
分隔符由 separator 属性控制,默认值是字符串 '/'(源码默认值见 Breadcrumbs.js)。CustomSeparator.tsx 演示了三种常见形式:
const breadcrumbs = [
<Link underline="hover" key="1" color="inherit" href="/" onClick={handleClick}>
MUI
</Link>,
<Link underline="hover" key="2" color="inherit" href="/material-ui/getting-started/installation/" onClick={handleClick}>
Core
</Link>,
<Typography key="3" sx={{ color: 'text.primary' }}>
Breadcrumb
</Typography>,
];
return (
<Stack spacing={2}>
<Breadcrumbs separator="›" aria-label="breadcrumb">
{breadcrumbs}
</Breadcrumbs>
<Breadcrumbs separator="-" aria-label="breadcrumb">
{breadcrumbs}
</Breadcrumbs>
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />} aria-label="breadcrumb">
{breadcrumbs}
</Breadcrumbs>
</Stack>
);
separator 的类型是 React.ReactNode(见 Breadcrumbs.d.ts),所以任何 React 节点都能作为分隔符。由于同一份 breadcrumbs 数组被三个组件复用,演示代码给每个子项都写了 key——这是把子项数组抽离复用时必须遵守的 React 规则。
从源码看,分隔符的插入逻辑在 insertSeparators(Breadcrumbs.js):它遍历子项数组,除最后一项外,在每项之后插入一个带 aria-hidden 的 <li> 分隔节点,键名是 separator-${index}。分隔节点自身的样式固定为 display: 'flex'、userSelect: 'none'、左右各 8px 外边距(Breadcrumbs.js),因此换用图标分隔符时会自然居中、且用户无法选中它。
四、带图标的面包屑
如果希望每个层级前带一个 Material 图标,官方做法是把图标放进链接内部,并用 display: 'flex', alignItems: 'center' 保证图标与文字垂直对齐(IconBreadcrumbs.tsx):
<Breadcrumbs aria-label="breadcrumb">
<Link
underline="hover"
sx={{ display: 'flex', alignItems: 'center' }}
color="inherit"
href="/"
>
<HomeIcon sx={{ mr: 0.5 }} fontSize="inherit" />
MUI
</Link>
<Link
underline="hover"
sx={{ display: 'flex', alignItems: 'center' }}
color="inherit"
href="/material-ui/getting-started/installation/"
>
<WhatshotIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Core
</Link>
<Typography sx={{ color: 'text.primary', display: 'flex', alignItems: 'center' }}>
<GrainIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Breadcrumb
</Typography>
</Breadcrumbs>
两个细节:fontSize="inherit" 让 SvgIcon 跟随父级 Typography 的字号缩放,mr: 0.5(即 4px)控制图标与文字的间距。
五、折叠过长路径:maxItems 与省略号展开
层级很深时(例如 Home / Catalog / Accessories / New Collection / Belts),一行放不下所有节点。Breadcrumbs 内建了折叠机制:通过 maxItems 限制显示数量,超出时保留头部 itemsBeforeCollapse 项与尾部 itemsAfterCollapse 项,中间放一个可点击的省略号按钮。
CollapsedBreadcrumbs.tsx 的用法非常直接:
<Breadcrumbs maxItems={2} aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="#">Home</Link>
<Link underline="hover" color="inherit" href="#">Catalog</Link>
<Link underline="hover" color="inherit" href="#">Accessories</Link>
<Link underline="hover" color="inherit" href="#">New Collection</Link>
<Typography sx={{ color: 'text.primary' }}>Belts</Typography>
</Breadcrumbs>
相关属性及默认值(摘自 Breadcrumbs.d.ts 与 Breadcrumbs.js):
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
maxItems |
number | 8 | 最多显示的面包屑数量;超过后只显示前 itemsBeforeCollapse 项与后 itemsAfterCollapse 项,中间用省略号代替 |
itemsBeforeCollapse |
number | 1 | 省略号前保留的项数 |
itemsAfterCollapse |
number | 1 | 省略号后保留的项数 |
expandText |
string | 'Show path' | 展开按钮的无障碍标签,可做国际化 |
separator |
ReactNode | '/' | 自定义分隔节点 |
component |
ElementType | 'nav' | 根元素使用的 HTML 元素或组件 |
slots.CollapsedIcon |
ElementType | 内置小图标 | 省略号按钮中的图标 |
slotProps.collapsedIcon |
object / function | {} | 作用在省略号图标上的 props |
源码里折叠的判定逻辑(Breadcrumbs.js)值得注意三点:
- 不触发折叠的条件:渲染时如果
expanded已为 true,或allItems.length <= maxItems,就渲染全部项,不出现省略号(Breadcrumbs.js)。 - 非法参数保护:如果
itemsBeforeCollapse + itemsAfterCollapse >= 总项数,说明本应全部显示,此时开发环境会console.error提示参数组合无效,并直接渲染全部项,避免插入无意义的省略号(Breadcrumbs.js)。 - 折叠态的省略号是一个真实按钮:它由 BreadcrumbCollapsed.js 渲染,
aria-label取expandText。点击后组件内部expanded状态翻转为 true 从而展开全部项,源码还特意处理了焦点回归——点击的按钮会从 DOM 中移除,于是把焦点移到列表内第一个可聚焦元素(a[href]、button或[tabindex])上,保证屏幕阅读器在 NVDA 下仍有播报(Breadcrumbs.js)。折叠态的图标可通过slots.CollapsedIcon替换、slotProps.collapsedIcon定制,内部走统一的useSlotProps合并逻辑(Breadcrumbs.js)。
一个容易踩的坑:children 不支持 React Fragment。源码在开发环境检测到 Fragment 子项会直接报错 "The Breadcrumbs component doesn't accept a Fragment as a child. Consider providing an array instead."(Breadcrumbs.js)。原因也顺理成章:面包屑依赖 React.Children.toArray(children) 逐项包裹 <li> 并数项数,Fragment 会破坏这一计数。
六、替代方案:用 Menu 收纳被省略的层级
折叠省略号适合"展开全部"的场景,但如果中间层级只需要跳转而不需要平铺展示,官方文档给出的替代方案是把中间层收进一个 Menu 下拉列表(breadcrumbs.md 中 "Condensed with menu" 一节)。CondensedWithMenu.tsx 的完整实现:
export default function CondensedWithMenu() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement> | null) => {
if (event) {
setAnchorEl(event.currentTarget);
}
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<React.Fragment>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
aria-labelledby="with-menu-demo-breadcrumbs"
>
<MenuItem onClick={handleClose}>Breadcrumb 2</MenuItem>
<MenuItem onClick={handleClose}>Breadcrumb 3</MenuItem>
<MenuItem onClick={handleClose}>Breadcrumb 4</MenuItem>
</Menu>
<Breadcrumbs aria-label="breadcrumbs">
<Link color="primary" href="#condensed-with-menu">
Breadcrumb 1
</Link>
<IconButton color="primary" size="small" onClick={handleClick}>
<MoreHorizIcon />
</IconButton>
<Link color="primary" href="#condensed-with-menu">
Breadcrumb 5
</Link>
<Link color="primary" href="#condensed-with-menu">
Breadcrumb 6
</Link>
</Breadcrumbs>
</React.Fragment>
);
}
这里面包屑里混入了一个 IconButton,它和 Link 一样被当作普通子项包进 <li>,分隔符会自动插在其两侧——也就是说 Breadcrumbs 对子项类型没有要求,任何合法元素都可参与。实际项目中通常根据路由深度动态决定 Menu 里放哪些 MenuItem。
七、深度定制:用 Chip 重绘面包屑外观
组件级别的深度定制走的是 MUI 的 slots / slotProps 与 styled 机制。官方演示 CustomizedBreadcrumbs.tsx 用 Chip 封装了一个"芯片式"面包屑:
const StyledBreadcrumb = styled(Chip)(({ theme }) => {
return {
backgroundColor: theme.palette.grey[100],
height: theme.spacing(3),
color: (theme.vars || theme).palette.text.primary,
fontWeight: theme.typography.fontWeightRegular,
'&:hover, &:focus': {
backgroundColor: emphasize(theme.palette.grey[100], 0.06),
...theme.applyStyles('dark', {
backgroundColor: emphasize(theme.palette.grey[800], 0.06),
}),
},
'&:active': {
boxShadow: theme.shadows[1],
backgroundColor: emphasize(theme.palette.grey[100], 0.12),
...theme.applyStyles('dark', {
backgroundColor: emphasize(theme.palette.grey[800], 0.12),
}),
},
...theme.applyStyles('dark', {
backgroundColor: theme.palette.grey[800],
}),
};
}) as typeof Chip; // TypeScript only: need a type cast here because https://github.com/Microsoft/TypeScript/issues/26591
function handleClick(event: React.MouseEvent<Element, MouseEvent>) {
event.preventDefault();
console.info('You clicked a breadcrumb.');
}
export default function CustomizedBreadcrumbs() {
return (
<div role="presentation" onClick={handleClick}>
<Breadcrumbs aria-label="breadcrumb">
<StyledBreadcrumb
component="a"
href="#"
label="Home"
icon={<HomeIcon fontSize="small" />}
/>
<StyledBreadcrumb component="a" href="#" label="Catalog" />
<StyledBreadcrumb
label="Accessories"
deleteIcon={<ExpandMoreIcon />}
onDelete={handleClick}
/>
</Breadcrumbs>
</div>
);
}
这段代码体现了 MUI 定制体系的三个要点:
theme.vars || theme写法同时兼容 CSS 变量模式与 JS 主题对象两种取色方式;theme.applyStyles('dark', {...})声明深色模式下的背景切换,emphasize用于在悬停/激活态加深背景;- 末尾
as typeof Chip的类型断言是因为styled对带as/component透传的组件存在 TS 已知限制(源码注释中引用了 Microsoft/TypeScript#26591)。
如果只想改样式而不换组件,也可以走全局 overrides:Breadcrumbs 是 OverridableComponent,组件名为 MuiBreadcrumbs,根元素样式可通过 components: { MuiBreadcrumbs: { styleOverrides: {...} } } 覆盖。类名方面,breadcrumbsClasses.ts 定义了 4 个工具类:MuiBreadcrumbs-root、MuiBreadcrumbs-ol、MuiBreadcrumbs-li、MuiBreadcrumbs-separator。值得注意的是,其 overridesResolver 把 li 的样式映射挂在了 root 的 overrides 里(Breadcrumbs.js),因此要在主题里定制每个列表项的样式,应写在 MuiBreadcrumbs 的 styleOverrides.li 下。
八、与 react-router 集成:按当前路径动态生成面包屑
生产环境里面包屑通常由路由派生,而不是手写。RouterBreadcrumbs.tsx 演示了完整的"路由驱动"方案,核心代码:
import {
Link as RouterLink,
Route,
Routes,
MemoryRouter,
useLocation,
} from 'react-router';
const breadcrumbNameMap: { [key: string]: string } = {
'/inbox': 'Inbox',
'/inbox/important': 'Important',
'/trash': 'Trash',
'/spam': 'Spam',
'/drafts': 'Drafts',
};
function LinkRouter(props: LinkRouterProps) {
return <Link {...props} component={RouterLink as any} />;
}
function Page() {
const location = useLocation();
const pathnames = location.pathname.split('/').filter((x) => x);
return (
<Breadcrumbs aria-label="breadcrumb">
<LinkRouter underline="hover" color="inherit" to="/">
Home
</LinkRouter>
{pathnames.map((value, index) => {
const last = index === pathnames.length - 1;
const to = `/${pathnames.slice(0, index + 1).join('/')}`;
return last ? (
<Typography key={to} sx={{ color: 'text.primary' }}>
{breadcrumbNameMap[to]}
</Typography>
) : (
<LinkRouter underline="hover" color="inherit" to={to} key={to}>
{breadcrumbNameMap[to]}
</LinkRouter>
);
})}
</Breadcrumbs>
);
}
这个模式由四个部分组成,均可直接迁移到自己的项目中:
- 路径名映射表
breadcrumbNameMap:把 URL 段映射为展示文案,未命中的段可以回退到原始段名; LinkRouter包装:把 react-router 的Link通过component属性注入 MUILink,从而同时获得路由跳转与 MUI 样式;演示中as any只是规避 TS 类型摩擦;useLocation派生子项:location.pathname.split('/').filter((x) => x)得到各级段,逐级用pathnames.slice(0, index + 1).join('/')拼出"前缀路径",保证每级都指向真实可导航的 URL(例如/inbox/important的上一级指向/inbox而非空路径);- 末项特殊处理:最后一段渲染为
Typography纯文本,其余段渲染为LinkRouter,与第二节的"活跃末项"策略一致(此处取"不可点击"变体)。
演示外层还用了 MemoryRouter initialEntries={['/inbox']} 与一个可折叠的邮箱文件夹 List(ListItemButton component={RouterLink} to={...} + Collapse)来模拟真实页面,方便验证路由切换后面包屑的同步更新。
九、DOM 结构与无障碍要求
从源码结构看,组件最终渲染的 DOM 形如:
<nav class="MuiBreadcrumbs-root"> <!-- 默认 component="nav",即 Typography 的 MuiBreadcrumbs-root -->
<ol class="MuiBreadcrumbs-ol"> <!-- flex + wrap,无 padding/margin,listStyle: none -->
<li class="MuiBreadcrumbs-li">…link…</li>
<li aria-hidden class="MuiBreadcrumbs-separator">/</li>
<li class="MuiBreadcrumbs-li">…</li>
</ol>
</nav>
其中根元素是一个 styled(Typography) 派生的 BreadcrumbsRoot,固定以 color="textSecondary" 渲染,因此面包屑默认文字为次级色、层级链接靠悬停下划线区分(Breadcrumbs.js 与 Breadcrumbs.js);<ol> 使用 flex 布局并允许换行(Breadcrumbs.js)。
官方文档 的 Accessibility 一节给出了明确的落地清单,该组件的可访问性依赖:
- 有序列表结构:链接集合使用
<ol>组织,屏幕阅读器能识别出顺序; - 分隔符对辅助技术隐藏:所有分隔节点带
aria-hidden,避免读到无意义的/或›; - 导航地标:根元素默认是带
aria-label的<nav>,把面包屑标识为导航地标,用户在辅助技术里可以按"landmark"快速定位。
因此每条面包屑示例都写作 <Breadcrumbs aria-label="breadcrumb">(个别演示用 aria-label="breadcrumbs")。若需要本地化的展开文案,把 expandText 与 aria-label 一起纳入翻译资源即可;组件同时支持通过 useDefaultProps(name: 'MuiBreadcrumbs')读取 DefaultPropsProvider 注入的默认 props(Breadcrumbs.js),适合全站统一配置。
十、小结:选型建议
结合本文演示的八种形态,可以归纳出一套实用的选型路径:
- 常规页面层级展示 → 基本用法,末项用
Typography; - 末项需要可点击 →
Link+aria-current="page"; - 视觉风格 →
separator换字符串或图标,或整体换Chip芯片外观; - 层级过深(超过
maxItems,默认 8)→ 用内建省略号折叠(itemsBeforeCollapse/itemsAfterCollapse/expandText),或用Menu下拉收纳中间层; - SPA 路由项目 → 按
useLocation派生子项 +component={RouterLink}的Link包装; - 无障碍 → 始终设置
aria-label,依赖组件内建的<ol>与aria-hidden分隔符结构。
组件源码位于 packages/mui-material/src/Breadcrumbs 目录,行为约束(折叠参数校验、Fragment 子项拒绝等)另有测试文件 Breadcrumbs.test.js 与 BreadcrumbCollapsed.test.js 覆盖,可直接阅读以验证边界行为。
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 StartedRust0623
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