必须同步更新aria-selected和aria-controls,仅设role="tab"和role="tabpanel"无效;aria-controls值须为对应tabpanel的唯一id,aria-labelledby须反向指向tab的id,二者构成双向语义链。

标签页切换必须同步 aria-selected 和 aria-controls
只写 role="tab" 和 role="tabpanel" 是无效的。屏幕阅读器会识别“这是个标签页”,但无法知道当前选中哪个、内容区对应哪个——除非你用 aria-selected 明确标出激活态,并用 aria-controls 建立按钮与面板的显式关联。
常见错误现象:
– 点击标签后视觉高亮,但读屏器仍报“未选中”
– 按 Tab 键跳到内容区时,读不出标题或上下文
– 切换标签后,焦点没落到对应 tabpanel,键盘用户卡在空白处
-
aria-controls的值必须是目标tabpanel元素的id,且该id必须真实存在、已渲染到 DOM 中(不能是 JS 生成后尚未 append 的) - 每个
tab必须有唯一id,每个tabpanel也必须有唯一id,否则aria-controls会指向错误节点或失效 - 初始状态要设
aria-selected="true"在默认激活的 tab 上,其余为false;JS 切换时必须同步更新这两个属性,不能只改 class 或 CSS
为什么不能只靠 role="tablist" 就完事?
role="tablist" 只告诉辅助技术“这是一组标签”,不包含任何状态或关系信息。W3C ARIA 实践规范明确要求:完整语义链 = tablist + 每个 tab 的 aria-selected + aria-controls + 对应 tabpanel 的 aria-labelledby(指向该 tab 的 id)。
容易踩的坑:
– 把 aria-controls 写成 class 名或 data-id,而不是真实 id
– 在 tabpanel 上漏掉 tabindex="-1",导致按 Tab 键跳过内容区
– 用 display: none 隐藏非活动面板,但没加 aria-hidden="true",读屏器仍会朗读所有内容
- 非活动
tabpanel应同时设置aria-hidden="true"和hidden(或display: none),避免被朗读 - 激活的
tabpanel必须有tabindex="-1",并在 JS 切换后调用.focus(),确保键盘用户可立即操作内容 - 不要用
aria-label替代aria-labelledby:前者会覆盖内部文本,后者能复用已有标题语义
aria-controls 和 aria-labelledby 怎么配对才安全?
aria-controls 是“我控制谁”,aria-labelledby 是“谁在定义我”。二者不是互换关系,而是分工协作:前者让屏幕阅读器知道点击这个 tab 会影响哪个区域,后者让那个区域知道自己是由哪个 tab 命名的。
示例结构必须长这样:
<div role="tablist"> <button role="tab" id="tab-1" aria-controls="panel-1" aria-selected="true">概览</button> <button role="tab" id="tab-2" aria-controls="panel-2" aria-selected="false">详情</button> </div> <div id="panel-1" role="tabpanel" aria-labelledby="tab-1" tabindex="-1">...</div> <div id="panel-2" role="tabpanel" aria-labelledby="tab-2" aria-hidden="true" tabindex="-1">...</div>
-
aria-labelledby的值必须是tab的id,不能是其他任意字符串 - 如果 tab 文本是动态生成的(比如从 API 拉取),确保
id也在同一时机生成并插入 DOM,不能延迟 - 禁用
tab时,除了加aria-disabled="true",还要移除aria-controls和aria-selected,否则语义冲突
键盘导航没响应?检查 aria-activedescendant 是否被误用
只有当标签页支持方向键(←→)切换且不改变焦点位置时,才需要 aria-activedescendant。普通点击切换场景下,它不仅多余,还会干扰默认行为——尤其在 Safari + VoiceOver 组合中,常导致焦点丢失或重复朗读。
真实使用场景极少:
– 标签数量极多(>20),需避免频繁 focus 移动影响性能
– 自定义 tablist 使用了 roving tabindex 模式(即只给当前项 tabindex="0",其余为 -1)
- 绝大多数项目只需用
click+keydown(ArrowLeft/ArrowRight)触发click即可,不用aria-activedescendant - 若真要用,必须配合
tabindex="-1"在tablist容器上,并手动管理aria-activedescendant值指向当前高亮 tab 的id - 切忌在
tab元素上同时设tabindex="0"和aria-activedescendant,语义矛盾
aria-controls 指向的 id 在组件 mount 完成前就已被引用,或在销毁时未清理,导致后续交互中属性指向空节点。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











