必须用javascript手动同步更新aria-selected,仅靠css类无效;它只在role="tab"元素上生效,且须置于role="tablist"内,同时需配合aria-controls、aria-hidden及aria-labelledby双向绑定,并确保焦点管理正确。

必须用 JavaScript 手动更新 aria-selected,仅靠 CSS 类切换或 DOM 属性静态设置无效——屏幕阅读器完全不识别 .active 这类 class,也不会自动把点击事件映射为选中态。
tab 元素上设 aria-selected 的前提条件
它只在 role="tab" 元素上生效,且该元素必须包裹在 role="tablist" 容器内。常见错误是把 aria-selected 写在 <div> 或 <code><span></span> 上,或者漏掉 role="tab" —— 即使加了属性,屏幕阅读器也会直接忽略。
-
role="tab"必须显式写在触发切换的可聚焦元素上(推荐用<button role="tab"></button>,不用<a></a>) -
tablist容器需有tabindex="0",确保键盘可进入 - 每个
tab必须带aria-controls="panel-id",指向对应tabpanel的id
切换时 JS 同步更新的最小安全操作
不能只改一个元素的 aria-selected="true",必须“清空 + 设置”两步到位。否则会出现多个 tab 同时为 true,破坏键盘导航逻辑(如 ←/→ 键跳过项、焦点丢失)。
- 先遍历所有
[role="tab"],统一移除aria-selected属性(或设为"false") - 再给目标 tab 元素设
aria-selected="true" - 同步更新对应
tabpanel:设aria-hidden="false",并确保其tabindex="0"或内部首个可交互元素获得焦点 - 其余
tabpanel设aria-hidden="true",并移除tabindex或设为-1
为什么 event.preventDefault() 和 focus() 不可省略
很多实现只监听 click 并改 class,但键盘用户按 ←/→ 或 Enter 时,事件可能没触发、焦点没落到目标 tab 上,导致屏幕阅读器仍读“未选中”。
- 监听
keydown时,对ArrowLeft/ArrowRight要调用nextTab.focus(),不能只改状态 - 对
Enter或Space,应显式调用tab.click()或复用已有 click 处理器,避免逻辑分裂 - 切换完成后,焦点必须落在当前
tabpanel内(推荐panel.querySelector('button, input, [tabindex]:not([tabindex="-1"])')?.focus())
最易被忽略的是 aria-labelledby 与 aria-controls 的双向绑定——漏掉任一端,NVDA 或 VoiceOver 就无法建立 tab 和 panel 的语义关联,即使 aria-selected 更新了,用户也听不出“这个面板是由哪个标签控制的”。











