aria-activedescendant 必须与 focus() 配合使用,仅设置属性不触发焦点或键盘操作;需同步更新目标元素 focus、aria-activedescendant 值及 scrollIntoView,并确保容器角色合法、ID 稳定唯一。

aria-activedescendant 必须配合 focus() 才生效
只写 aria-activedescendant="option-2" 不会让元素获得焦点,也不会触发键盘操作。它只是告诉屏幕阅读器“当前逻辑高亮的是哪个选项”,而真实焦点仍需 JS 主动调用 element.focus()。
常见错误是:设了属性但没调用 focus() → 键盘按方向键无反应;或调用了 focus() 但没同步更新 aria-activedescendant → 屏幕阅读器播报错项。
- 必须同时做两件事:找到目标
role="option"元素,调用它的focus(),再把它的id写进触发按钮(role="combobox")的aria-activedescendant属性里 - 不能用
dataset.id或临时生成的字符串代替真实 DOM 元素的id值 - 若目标项在视口外,要提前调用
element.scrollIntoView({ block: 'nearest' }),否则焦点存在但不可见
容器和子项角色必须严格匹配 WAI-ARIA 规范
aria-activedescendant 不是随便加在哪都能用的。它只在特定容器角色下被辅助技术识别,比如 role="listbox"、role="combobox"、role="grid" 等。加在 role="menu" 或普通 div 上会直接报错或静音。
浏览器开发者工具的无障碍树里如果显示 “ARIA container role missing” 或类似提示,基本就是这个原因。
立即学习“前端免费学习笔记(深入)”;
- 父容器必须有合法容器角色:
role="listbox"(最常用)、role="combobox"(配合aria-controls指向 listbox) - 所有可选项必须是
role="option",且每个都有唯一、稳定、已挂载的id - 触发按钮(如输入框或下拉箭头)必须设置
aria-controls="listbox-id",指向 listbox 容器的id - 如果下拉内容通过
innerHTML = ...动态替换,旧id会丢失 →aria-activedescendant指向空节点,读屏器报 “ID not found”
方向键移动时容易漏掉的三步同步
监听 ArrowDown / ArrowUp 后,光更新一个地方远远不够。必须原子化完成三件事,缺一不可。
- 用
querySelectorAll('[role="option"]')获取全部选项,但要过滤掉aria-hidden="true"或display: none的项(否则索引错乱) - 计算下一个/上一个有效项索引,注意循环:到末尾再按 ↓ 应回到第一个,到开头再按 ↑ 应跳到最后一个
- 对目标元素执行:
element.focus()→ 更新触发按钮的aria-activedescendant→ 调用element.scrollIntoView()(如有必要)
很多实现卡在第一步——用 querySelectorAll 拿到一堆元素,却没剔除隐藏项,导致方向键跳过某些选项或越界报错。
初始展开和收起菜单时的状态清理
初始状态和收起后最容易被忽略,但恰恰影响首次交互体验。
- 第一次展开下拉时,如果不手动设置
aria-activedescendant,屏幕阅读器默认播报第一个option,但焦点实际还在触发按钮上 → 用户按 ↓ 才开始移动,体验断层 - 收起菜单后,不把触发按钮上的
aria-activedescendant清成空字符串或删掉该属性 → 下次展开时读屏器仍报上次的项,哪怕数据已刷新 - 初始聚焦建议:展开后立即聚焦第一个可见
option,并同步设aria-activedescendant;收起前清空该属性,并将焦点移回触发按钮(保持 tab 顺序连贯)
真正难的不是写对一次,而是每次 DOM 变更、每次用户操作、每次状态切换,都保证两套状态(视觉焦点 + 语义高亮)始终一致。稍有松懈,键盘用户和读屏用户就会看到/听到完全不同的东西。



















