aria-activedescendant 本身不聚焦元素,仅告知屏幕阅读器当前逻辑高亮项,必须与 focus() 同步更新;二者不同步会导致键盘操作与读屏播报错位。

aria-activedescendant 不是用来“聚焦元素”的,而是告诉屏幕阅读器“当前逻辑上高亮的选项是哪一个”,它必须配合手动焦点管理使用。只设这个属性但不调用 focus(),键盘用户依然无法操作;只调用 focus() 但没同步更新 aria-activedescendant,读屏器会朗读错误项。
为什么不能只靠 focus() 或只靠 aria-activedescendant
原生 <select></select> 内部焦点和语义由浏览器托管,但自定义 role="combobox" + role="listbox" 组合必须自己维护两套状态:
-
document.activeElement指向当前获得真实焦点的 DOM 元素(比如某个<li role="option">) -
aria-activedescendant属性值必须是该元素的id字符串(如"option-2"),且该元素必须存在于 DOM 中 - 两者不同步 → 键盘用户按方向键时焦点跳转了,但读屏器仍播报旧项;或读屏器报对了,但键盘无法操作那个“逻辑高亮项”
设置 aria-activedescendant 的三个硬性前提
这个属性不是加了就生效,它依赖完整语义链:
- 父容器必须有
role="listbox"(不能是role="menu"或role="group") - 所有可选子项必须是
role="option",且每个都有唯一id(不能用data-id或动态生成未挂载的 ID) - 触发按钮(
role="combobox")必须同时设置aria-controls="listbox-id",指向listbox容器的id - 若下拉内容通过
innerHTML = ...替换,旧id会丢失 →aria-activedescendant指向无效节点,读屏器静音或报错“ID not found”
方向键移动时如何同步更新
监听 ArrowDown/ArrowUp 后,需同时做三件事:
- 找到下一个/上一个
role="option"元素(注意循环:到末尾再按 ↓ 应回到第一个) - 调用
element.focus()让其获得真实焦点 - 更新触发按钮上的
aria-activedescendant值为该元素的id(不是dataset.id,必须是element.id) - 如果目标选项在滚动区域外,要提前
element.scrollIntoView({ block: 'nearest' }),否则焦点虽在但不可见
容易被忽略的边界情况
多数实现卡在这些细节上:
- 初始展开时没设置任何
aria-activedescendant→ 读屏器默认播报第一个option,但焦点实际在触发按钮上,用户按 ↓ 才开始移动 - 收起菜单后没清空
aria-activedescendant→ 下次展开时读屏器仍报上次的项,即使已刷新数据 - 用
querySelectorAll('[role="option"]')获取选项,但其中某些option被display: none或aria-hidden="true"隐藏 → 它们不该参与导航,需过滤掉 - 移动端 Safari 对
aria-activedescendant支持不稳定,更依赖真实focus(),所以不能省略element.focus()调用
真正麻烦的不是写那行 button.setAttribute('aria-activedescendant', id),而是保证每次 DOM 变化、每次键盘输入、每次滚动可视区更新时,这个值都准确对应当前视觉高亮且可聚焦的 option 元素 —— 差一个 ID 就会让屏幕阅读器用户迷失。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











