首页
/ Material UI Breadcrumbs 组件实战:React 面包屑导航的完整用法、折叠机制与源码解析

Material UI Breadcrumbs 组件实战:React 面包屑导航的完整用法、折叠机制与源码解析

2026-09-05 18:59:48作者:翟萌耘Ralph

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>
  );
}

几个关键约定值得注意:

  • 中间层级用 Linkunderline="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 规则。

从源码看,分隔符的插入逻辑在 insertSeparatorsBreadcrumbs.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.tsBreadcrumbs.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)值得注意三点:

  1. 不触发折叠的条件:渲染时如果 expanded 已为 true,或 allItems.length <= maxItems,就渲染全部项,不出现省略号(Breadcrumbs.js)。
  2. 非法参数保护:如果 itemsBeforeCollapse + itemsAfterCollapse >= 总项数,说明本应全部显示,此时开发环境会 console.error 提示参数组合无效,并直接渲染全部项,避免插入无意义的省略号(Breadcrumbs.js)。
  3. 折叠态的省略号是一个真实按钮:它由 BreadcrumbCollapsed.js 渲染,aria-labelexpandText。点击后组件内部 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.tsxChip 封装了一个"芯片式"面包屑:

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:BreadcrumbsOverridableComponent,组件名为 MuiBreadcrumbs,根元素样式可通过 components: { MuiBreadcrumbs: { styleOverrides: {...} } } 覆盖。类名方面,breadcrumbsClasses.ts 定义了 4 个工具类:MuiBreadcrumbs-rootMuiBreadcrumbs-olMuiBreadcrumbs-liMuiBreadcrumbs-separator。值得注意的是,其 overridesResolverli 的样式映射挂在了 root 的 overrides 里(Breadcrumbs.js),因此要在主题里定制每个列表项的样式,应写在 MuiBreadcrumbsstyleOverrides.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>
  );
}

这个模式由四个部分组成,均可直接迁移到自己的项目中:

  1. 路径名映射表 breadcrumbNameMap:把 URL 段映射为展示文案,未命中的段可以回退到原始段名;
  2. LinkRouter 包装:把 react-router 的 Link 通过 component 属性注入 MUI Link,从而同时获得路由跳转与 MUI 样式;演示中 as any 只是规避 TS 类型摩擦;
  3. useLocation 派生子项location.pathname.split('/').filter((x) => x) 得到各级段,逐级用 pathnames.slice(0, index + 1).join('/') 拼出"前缀路径",保证每级都指向真实可导航的 URL(例如 /inbox/important 的上一级指向 /inbox 而非空路径);
  4. 末项特殊处理:最后一段渲染为 Typography 纯文本,其余段渲染为 LinkRouter,与第二节的"活跃末项"策略一致(此处取"不可点击"变体)。

演示外层还用了 MemoryRouter initialEntries={['/inbox']} 与一个可折叠的邮箱文件夹 ListListItemButton 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.jsBreadcrumbs.js);<ol> 使用 flex 布局并允许换行(Breadcrumbs.js)。

官方文档 的 Accessibility 一节给出了明确的落地清单,该组件的可访问性依赖:

  • 有序列表结构:链接集合使用 <ol> 组织,屏幕阅读器能识别出顺序;
  • 分隔符对辅助技术隐藏:所有分隔节点带 aria-hidden,避免读到无意义的 /
  • 导航地标:根元素默认是带 aria-label<nav>,把面包屑标识为导航地标,用户在辅助技术里可以按"landmark"快速定位。

因此每条面包屑示例都写作 <Breadcrumbs aria-label="breadcrumb">(个别演示用 aria-label="breadcrumbs")。若需要本地化的展开文案,把 expandTextaria-label 一起纳入翻译资源即可;组件同时支持通过 useDefaultPropsname: '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.jsBreadcrumbCollapsed.test.js 覆盖,可直接阅读以验证边界行为。

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