nav 必须用 ul/li 包裹链接,不能直接放 a 或 div;多 nav 需 aria-label 区分;SPA 中 href 应用 # 路径并由 JS 拦截;当前页链接必须加 aria-current="page"。

nav 里必须放 ul + li,不能只包 a 标签
屏幕阅读器靠 <ul></ul> 判断“这是一组并列导航项”,不是靠 <nav></nav> 标签名。平铺写 <nav><a>首页</a><a>产品</a></nav>,读屏器会当成两段独立文本,无法告知用户“共 2 个导航项”。
常见错误现象:<nav><div>首页</div>
<div>产品</div></nav> —— 键盘 Tab 焦点顺序混乱,Lighthouse 报 “Landmark lacks list structure”。
正确结构要点:
-
<nav></nav>内部必须用<ul></ul>或<ol></ol>包裹所有链接 - 每个链接单独放在
<li>里,不要用<div> 或 <code><span></span> - 二级菜单用嵌套
<ul></ul>,不是嵌套<nav></nav>(<nav><ul><li>产品<ul><li>云服务</li></ul> </li></ul></nav>) - 静态站点/多页应用:用相对路径
href="contact.html"或根相对路径href="/blog" - SPA(React/Vue):统一用
href="#products"或href="#",再由 JS 拦截并调用router.push() - 外部链接:必须带协议,
href="https://example.com",不能写href="example.com" - 邮箱链接:严格写成
href="mailto:someone@example.com" - 用
aria-label直接说明用途:<nav aria-label="主导航"></nav>、<nav aria-label="页脚快捷入口"></nav> - 如果已有可见标题(如
<h2 id="related">相关栏目</h2>),改用aria-labelledby="related" - 禁用模糊表述:
aria-label="菜单"、aria-label="导航"—— 对辅助技术用户等于没标 - 静态页:在当前页对应链接上加
aria-current="page",如<a href="/" aria-current="page">首页</a> - SPA:路由切换后,JS 必须动态更新该属性,不能只靠 class 控制视觉高亮
- 禁止只写
aria-current不写值,或写成aria-current="true"—— 只有"page"、"step"、"location"是合法值
href 值写错会导致 SPA 白屏或 SEO 失效
单页应用里写 href="about.html" 是最常踩的坑:点击后整页重载,Vue Router 报 NavigationDuplicated,React Router 的 history stack 断掉,返回按钮失效。
立即学习“前端免费学习笔记(深入)”;
真实部署环境还要区分路径类型:
验证方法:在控制台运行 document.querySelectorAll('nav a[href]'),确保返回的 NodeList 里每个 href 都可被浏览器解析为有效跳转目标。
多个 nav 共存时,aria-label 不是可选项
一个页面有顶部主导航、侧边栏目导航、页脚快捷入口,全用 <nav></nav> 但不加标识,屏幕阅读器会把它们都读作“导航”,用户根本分不清哪个是主菜单、哪个是辅助跳转。
W3C 明确要求多导航区必须可区分:
注意:<nav></nav> 本身不支持嵌套,别试图用 <nav><nav></nav></nav> 分级;下拉逻辑靠 CSS/JS 实现,语义靠嵌套 <ul></ul> 表达。
nav 不是样式容器,当前页高亮要靠 aria-current="page"
<nav></nav> 不提供任何视觉样式或交互能力。CSS 写 nav a:hover 可以悬停变色,但用户用键盘 Tab 进入导航区后,完全不知道当前在哪一页——除非你手动加 aria-current="page"。
这个属性不是装饰,是 WCAG 2.1 强制要求的可访问性信号:
最容易被忽略的是:很多团队花时间调下拉动画,却漏掉 aria-current,导致键盘用户每次都要点开才能确认自己是否在目标页。



















