<nav>需包裹<ul><li>结构并配aria-label和aria-current="page"才能通过无障碍校验;语义边界模糊和DOM位置错误(如<nav>内嵌<div>、<main>非body直接子元素、缺失lang属性)才是失效主因。

直接用 <nav> 包裹、配 <ul><li><a> 结构、加上 aria-label 和 aria-current="page",就能让绝大多数导航条通过基础无障碍校验。但真正卡住人的,是语义边界模糊和 DOM 位置错误——不是“没加属性”,而是“加了却不起作用”。
为什么 <nav> 套 <div> 还是没用
常见现象:导航看着能点、能高亮,但 NVDA 按 N 键跳不过去,VoiceOver 列表里不显示“主导航”。根本原因不是没写 <nav>,而是里面塞了 <div class="menu-item"> 或直接平铺 <a>,没走 <ul><li> 路径。
-
<nav>只定义区域类型,不定义内部结构;屏幕阅读器靠<ul>识别“这是个可计数的链接列表”,否则把所有<a>连读成一串无标点文本 - 别用
<div>模拟<li>,哪怕加了role="listitem",兼容性也远不如原生标签 - 面包屑必须用
<ol>,不是因为“要编号”,而是<ol>会播报“第1项/共3项”,<ul>只报“列表,共3项”
aria-label 不是可选填项,而是导航区身份证
多个 <nav> 共存时,不加 aria-label 就等于让读屏器用户面对三扇一模一样的门——按 N 键跳转后,无法判断哪扇是主导航、哪扇是页脚链接、哪扇是文章目录。
- 值必须具体:
aria-label="主导航"可行,aria-label="Navigation"或空着等于没标 - 面包屑固定用
aria-label="当前位置",这是 WCAG 推荐写法,NVDA/VoiceOver 对该值有特殊优化 - 不要给
<nav>加role="navigation"——冗余,部分旧读屏会覆盖原生语义
<main> 的位置错一个层级,跳过链接就失效
跳过链接(Skip Link)默认指向 <main id="main">,但如果 <main> 被包在 <div class="wrapper"> 或 <section> 里,NVDA 就认不出它是主内容区,键盘用户 Tab 之后依然卡在导航里。
立即学习“前端免费学习笔记(深入)”;
-
<main>必须是<body>的直接子元素,不能嵌套 - 跳过链接本身必须是 DOM 中第一个可聚焦元素(即写在
<header>之前),否则键盘用户还没碰到它就先 tab 进了 logo 或搜索框 - SPA 场景下,路由切换后需手动更新跳过链接的
href,否则它永远指向旧的#main
当前页高亮只靠 CSS 类,对读屏用户就是隐形的
用户看不到 class="active",但能听到“产品,链接”——如果没同步设置 aria-current="page",他们就不知道哪个是当前所在页面,尤其当多个导航项文字相同时(比如三个“详情”链接)。
- 必须双写:
<a href="/products" aria-current="page">产品</a>,不能只写 class -
aria-current合法值只有"page"、"step"、"location"等几个,aria-current="true"是无效值,部分读屏会忽略 - JS 动态更新时,改
class的同时必须同步操作aria-current属性,否则读屏状态滞后
最常被忽略的其实是 <html lang="zh-CN"> —— 缺失或写错这个,整个页面的中文朗读可能全乱套,导航再标准也没用。它不在导航结构里,却是所有语义生效的前提。



















