正确实现aria标签页必须用button[role="tab"]包裹文字,ul设role="tablist"并配aria-label;每个tab需tabindex、aria-selected和aria-controls,对应tabpanel的id与aria-labelledby严格匹配;键盘导航需手动js支持arrow/home/end键,并用aria-hidden+hidden隐藏非激活面板。

ul[role="tablist"] 必须用
仅给 ul 加 role="tablist",里面放 li + 文字,屏幕阅读器会把它读成普通列表,键盘也无法聚焦切换。ARIA 规范明确要求每个可交互 tab 必须是 role="tab",且推荐用原生 <button></button> 元素——它自带焦点、空格/回车触发、禁用状态语义。
常见错误写法:<ul role="tablist">
<li>首页</li>
<li>关于</li>
</ul>。正确结构必须是:
-
button是唯一推荐的承载role="tab"的元素;a标签会触发跳转,div缺少内置交互语义 - 每个
button必须显式设tabindex="0"(激活态)或tabindex="-1"(非激活态),不能依赖默认行为 -
ul容器本身**不能**设tabindex,否则会在键盘流中多出一个无意义的停靠点 -
aria-label或aria-labelledby必须存在,用于向屏幕阅读器说明这个 tablist 的用途
aria-controls 和 aria-labelledby 必须严格双向匹配
漏掉或值不一致,NVDA、VoiceOver 就无法把 tab 和 panel 关联起来——用户听到“首页”后,切换到空白区域,完全不知道内容在哪。这两个属性不是可选的装饰,而是屏幕阅读器理解控制关系的唯一依据。
关键规则:
-
button[role="tab"]的aria-controls值,必须与目标div[role="tabpanel"]的id**完全一致**(区分大小写、不可含空格、不可用 class 名代替) -
div[role="tabpanel"]的aria-labelledby必须指向对应button的id,形成可逆链接 - 示例配对:
aria-controls="panel-home"↔<div id="panel-home" role="tabpanel" aria-labelledby="tab-home">,其中 <code>tab-home是 button 的 id - 别用
data-tab-target或class模拟关联——辅助技术完全无视这些 - 在
ul[role="tablist"]上监听keydown,不要给每个button单独绑——避免重复和状态不同步 - 对
ArrowLeft/ArrowRight/Home/End调用event.preventDefault(),否则可能触发页面滚动 - 维护一个所有 tab
button的数组,用Array.prototype.indexOf(document.activeElement)找当前索引 - 循环逻辑要真实调用
.focus():从末尾按 → 必须移到第一个 DOM 节点,不能只改状态 - 不要用
document.activeElement判断“当前激活 tab”——鼠标点击后焦点已在 button 上,但 JS 状态可能滞后,应以aria-selected="true"为准 - 非激活面板设
aria-hidden="true"+hidden属性(二者建议同时用,兼容性更稳) - 视觉上用
clip-path: inset(50%)或position: absolute; clip: rect(1px, 1px, 1px, 1px)隐藏,而非移除渲染 - 激活面板必须设
aria-hidden="false",并确保其内部首个可聚焦元素(如h2、button)在切换后获得焦点(panel.firstElementChild?.focus()) - 每次切换,必须同步更新:
aria-selected(tab)、aria-hidden(panel)、tabindex(tab 和 panel 内容)三处状态 - 别用 CSS class 如
is-active替代aria-selected——屏幕阅读器完全忽略 class
键盘导航必须手写 JS,浏览器不自动实现 Arrow/ Home/ End
加了 role="tablist" 不等于有了方向键切换。Chrome 可能部分支持,但 Safari + VoiceOver 几乎完全不响应,必须监听 keydown 并手动管理焦点。
实操要点:
隐藏非激活面板不能只靠 display: none
用 display: none 或 visibility: hidden 隐藏未选中的 tabpanel,屏幕阅读器会直接跳过整个区域,即使你刚切过去,用户也听不到内容。WAI-ARIA 要求保留 DOM 结构,用语义化方式控制可访问性。
正确做法是组合使用:
role="tab" 对 Space 键的双重响应(按钮原生触发 + JS 再执行一次),以及移动端触摸后未显式调用 .focus() 导致键盘流中断。这些点不处理,整个 tablist 在 VoiceOver 下就等于没做无障碍。











