aria-controls 必须指向真实存在的唯一 id,且需与 aria-expanded 状态同步;目标面板应设 role="region" 和 aria-labelledby,动态加载时优先用 aria-labelledby 反向关联而非硬写 aria-controls。

aria-controls 必须指向真实存在的 ID
它不是装饰性属性,而是明确告诉屏幕阅读器“这个按钮控制哪块内容”。如果 aria-controls 的值拼错、ID 不存在、或目标元素被动态移除后没清理该属性,辅助技术会静默失败——用户听不到任何提示,也跳转不到对应区域。
- 目标
id必须全局唯一,且在 DOM 中真实存在(不能是 JS 临时生成但尚未挂载的 ID) - 多个按钮共用同一个
aria-controls值是允许的,但前提是它们确实控制同一块内容(如多个“展开”按钮都操作同一个面板) - 若目标面板是动态加载的(比如 Tab 切换时才插入 DOM),建议在面板渲染完成后再设置
aria-controls,或干脆省略它,改用aria-labelledby反向关联(更稳妥)
aria-controls 和 aria-expanded 必须状态同步
这两个属性要像齿轮咬合一样联动:按钮点开时,aria-expanded 得设为 "true",同时面板必须可见;收起时,aria-expanded 立即变为 "false",面板同步隐藏。不同步会导致读屏器播报“已展开”,但用户按 Enter 却没反应,或反之。
- 不要写成
aria-expanded=""或aria-expanded="undefined"—— 这些是无效值,会被忽略 - 初始状态必须和 DOM 实际一致:如果面板默认隐藏,按钮初始就得带
aria-expanded="false" - 用 JavaScript 切换时,优先用
setAttribute('aria-expanded', 'true'),别依赖 class 切换或 CSS 变量
面板容器要配 role 和 aria-hidden(可选但推荐)
仅靠 aria-controls 不足以让辅助技术理解“这块内容是什么、和谁有关”。面板本身需要语义锚点,否则读屏器可能只把它当普通 div 读。
- 给面板加
role="region",并用aria-labelledby指回触发按钮的id,建立双向语义链 - 当面板隐藏时,建议同步设
aria-hidden="true";显示时设为"false"或直接移除该属性 - 如果面板用
display: none隐藏,aria-hidden可省略(CSS 隐藏已有可访问性效果),但显式声明更可控、更易调试
动态 Tab 场景下可以不写 aria-controls
当 Tab 内容是懒加载、DOM 中只保留当前激活的 tabpanel 时,硬给每个 tab 都写 aria-controls 指向一个还不存在的 ID,反而会让读屏器报错或跳过。
- ARIA 规范明确说明:关联可用
aria-controls(在 tab 上)或aria-labelledby(在 tabpanel 上),二者选其一即可,非强制 - 此时更推荐把
tablist和当前tabpanel紧邻放置(tablist在前),并用aria-labelledby反向关联 - 避免为了“看起来规范”而堆砌无效属性——可访问性不是填空游戏,而是确保逻辑闭环
aria-expanded,或者面板 DOM 节点被 Vue/React 销毁后 aria-controls 还挂着,无障碍就断在那一环。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











