必须嵌套结构并添加aria-label,否则语义失效;SPA中href需适配路由,当前页用aria-current="page",交互须JS实现。

直接用 <nav></nav> 包裹链接不行,必须满足结构、语义、可访问性三重条件,否则浏览器和读屏器会当它不存在。
为什么 <nav></nav> 里只放 <a></a> 还是报错?
因为 <nav></nav> 本身不定义项的组织方式,它只声明“这是导航区”;真正被识别为“一组可遍历的导航项”的,是 <ul></ul> + <li> 结构。浏览器和屏幕阅读器靠这个组合确认项数、顺序和层级。
-
<nav><div>首页</div></nav>:会被降级为普通容器,键盘 Tab 焦点可能跳过,Lighthouse 检测报 “Landmark lacks list structure” -
<nav><ul><li><a href="/">首页</a></li></ul></nav>:读屏器播报“导航区域,列表,1 项:首页”,语义完整 - 嵌套二级菜单时,子
<ul></ul>必须放在<li>内,不能直接塞进外层<ul></ul>,否则 HTML 解析器会自动修正结构,行为不可控
多个 <nav></nav> 共存时,不加 aria-label 就等于没写
同一页面出现顶部主导航、页脚快捷入口、侧边相关文章链接,全用 <nav></nav> 是允许的,但屏幕阅读器只会统一读作“navigation”。用户根本分不清哪个是主菜单,哪个是辅助跳转。
- 正确:
<nav aria-label="主导航">...</nav>、<nav aria-label="页脚导航">...</nav> - 有可见标题时可用
aria-labelledby="header-title",比如<h2 id="header-title">网站导航</h2> - 绝对不要写
aria-label=""或仅靠 CSS 隐藏文字(如<span class="sr-only">主菜单</span>),部分读屏器会跳过整个区域
单页应用(SPA)里 href 写真实路径就翻车
在 React/Vue 项目中,<nav></nav> 仍需保留,但内部 <a></a> 的 href 不能指向 /about.html 这类物理路径——否则点击即整页刷新,路由状态丢失,Vue Router 报 NavigationDuplicated,React Router 也断开历史栈。
- 推荐写法:
<a href="#about">关于我们</a>(配合 hashchange 监听)或<a href="#" onclick="event.preventDefault(); router.push('/about')">关于我们</a> - Vue 项目优先用
<router-link></router-link>,但外层<nav aria-label="主导航"></nav>不可省,否则语义链断裂 - React 项目若用
<link>,确保它最终渲染为带href属性的<a></a>,否则读屏器无法识别为可聚焦导航项
当前页高亮和键盘焦点管理不是 <nav></nav> 的责任
<nav></nav> 只负责语义声明,不提供任何交互能力。当前页标记、下拉展开、Tab 键焦点流转、Enter 触发菜单,全部要靠 JS 实现。
- 激活页必须加
aria-current="page",例如:<a href="/products" aria-current="page">产品</a>,读屏器才读出“产品,当前页面” - 纯 CSS 的
:hover或:focus-within下拉在触摸设备上基本失效,移动端必须用 JS 控制aria-expanded和hidden属性 - 键盘导航需手动实现:Tab 进入菜单 → Enter 展开子项 → ArrowDown 移动焦点 → Esc 收起,这些逻辑不会自动产生
最容易被忽略的是:写完 <nav></nav> 后不验证 DOM 真实状态——用 document.querySelectorAll('nav a[href]') 确认链接存在且可访问,再开 NVDA 或 VoiceOver 实测朗读效果,比任何教程都管用。


















