面包屑导航必须用包裹并采用结构,末项用标识,分隔符通过css伪元素实现,路径需真实反映业务层级而非url机械拆分。

面包屑导航不是靠某个标签自动渲染出来的视觉组件,它必须是语义准确、路径真实、可被辅助技术识别的导航链路。用 <div> 堆文字、用 <code><ul></ul> 包链接、靠 window.location.pathname.split('/') 硬切路径——这三类写法在 Lighthouse、axe 或屏幕阅读器下基本都过不了关。
为什么必须用 <nav aria-label="Breadcrumb"></nav> 包裹
没有 <nav></nav> 容器,屏幕阅读器会把面包屑当成普通段落读,用户无法感知这是导航路径;不加 aria-label="Breadcrumb",部分读屏工具甚至不会将其归类为“导航”类型。常见错误包括:
- 用
<p></p>或<div class="breadcrumb"> 替代 <code><nav></nav> - 写了
<nav></nav>但漏掉aria-label,或写成aria-labelledby(需额外绑定 ID,多此一举) - 在单页应用中,路由切换后忘记更新
aria-label的值(其实不用动,固定写死即可) - Lighthouse 会将
<ul></ul>写法标记为“语义缺失” - 即使你用
list-style: none隐藏了数字,<ol></ol>的语义依然有效 - 末项必须用
<li aria-current="page">,不能只加class="active" - ❌ 不要用纯文本
/或>直接写在 HTML 里(会被读屏工具读出来) - ❌ 不要用
<span>/</span>(语义冗余,且可能被聚焦) - ✅ 正确做法:用
li::before { content: "/"; aria-hidden="true" },并确保第一个li的伪元素被清除 - 注意:如果用了
aria-hidden="true",就别再给伪元素加role或其他 ARIA 属性 - 静态站(Jekyll/Hugo):靠模板变量注入,比如
{{ page.breadcrumb }},不是靠 JS 解析 - Vue Router:用
useRoute().matched遍历,取每层meta.title和path,不能靠path.split('/') - React Router v6:用
useMatches(),重点看handle?.title,动态参数(如/user/:id)的标题必须在路由定义时预置 - 所有场景下,当前页的
href必须为空或省略,且必须带aria-current="page"
<ol></ol> 而不是 <ul></ul> 的硬性原因
面包屑有明确层级顺序:首页 → 分类 → 子分类 → 当前页。这个“先后”是语义本身,不是 CSS 控制的视觉流。<ol></ol> 会被屏幕阅读器读出序号(如“第 1 项 首页,第 2 项 分类…”),而 <ul></ul> 表示无序集合,直接破坏路径逻辑。
分隔符该用什么:伪元素还是文本?
分隔符必须是视觉呈现,不能参与语义或朗读。所以:
静态页与 SPA 的路径来源根本不同
纯静态 HTML 页面没有路由系统,document.location.pathname 只能告诉你 URL 长什么样,但无法推导出“首页 > 文档 > 面包屑”这样的业务层级。这时候:
最容易被忽略的是:面包屑的层级必须和网站真实的导航结构对齐,而不是 URL 路径看起来像几级就是几级。比如 /blog/2026/05/breadcrumb 实际归属“首页 > 博客 > 技术指南”,那面包屑就得按这个来,而不是硬拆成五级。











