role="listbox"仅适用于自定义实现的、需键盘导航和可访问性支持的下拉面板(如模拟),不可用于原生;必须配role="option"、tabindex="0"、aria-labelledby或aria-label,并手动实现焦点切换、选中同步等交互逻辑。

role="listbox" 什么时候该用,什么时候不该用
不是所有下拉列表都适合用 role="listbox"。它专指可键盘导航、支持多选/单选、且选项始终可见(比如自定义 <div> 实现的下拉面板),而不是原生 <code><select></select>。浏览器对 role="listbox" 的 ARIA 支持依赖于正确配对 role="option" 和焦点管理,漏掉任一环节就会导致屏幕阅读器读不出选项或无法用方向键切换。
- 原生
<select></select>不需要、也不应该加role="listbox"—— 它自带语义和键盘行为 - 用
role="listbox"时,必须给每个选项加role="option",且至少一个选项带aria-selected="true" - 容器需有
tabindex="0"才能获得键盘焦点;若支持多选,还要加aria-multiselectable="true"
必须手动实现的交互逻辑有哪些
role="listbox" 不自动提供任何行为 —— 它只声明语义,不绑定事件。你得自己写 JS 处理:方向键上下切换焦点、Enter/Space 触发选中、Home/End 跳转首尾、以及 aria-selected 的同步更新。
- 监听
keydown,拦截ArrowUp/ArrowDown,并调用element.focus()切换到对应role="option" - 选中时不仅要设
aria-selected="true",还得移除其他选项的该属性(单选场景) - 若选项动态增删,必须重新计算可聚焦项,否则
Tab键可能跳过整个 listbox
const options = container.querySelectorAll('[role="option"]');
options.forEach(opt => {
opt.addEventListener('click', () => {
options.forEach(o => o.setAttribute('aria-selected', 'false'));
opt.setAttribute('aria-selected', 'true');
});
});
常见错误:为什么屏幕阅读器念不出来
最常踩的坑是缺了 aria-labelledby 或 aria-label。没有标签,屏幕阅读器只会说“列表框”,不读内容。另一个高频问题是把 role="listbox" 套在 <select></select> 上,造成语义冲突,部分 NVDA 版本直接忽略内部 option。
- 必须用
aria-labelledby="id-of-label"指向一个可见文本标签,或用aria-label="请选择城市" - 不要嵌套
role="listbox"在<form></form>或<fieldset></fieldset>里再加role="group"—— 层级混乱会打断朗读流 - 避免在
role="option"里再放div包裹文字;最好直接用<span></span>或纯文本,否则需额外加aria-hidden="true"过滤装饰节点
与原生 select 对比:什么情况下值得自己造轮子
只有当你需要高度定制样式、异步加载选项、组合搜索 + 下拉、或集成虚拟滚动时,才考虑 role="listbox"。否则原生 <select></select> 更轻量、兼容性更好、无需维护焦点逻辑。
- 移动端 Safari 对
role="listbox"的触摸支持不稳定,经常无法唤出软键盘或触发点击 - Chrome 和 Firefox 对
aria-activedescendant的支持比直接focus()更一致,但需要额外维护活动项 ID - 如果项目要支持 IE11,别用
role="listbox"—— 其 ARIA 支持残缺,推荐降级为原生select或用 polyfill 库如ally.js
真正难的不是写对 ARIA 属性,而是让键盘、鼠标、触摸、屏幕阅读器四套交互路径全部走通且不互相干扰。一个没处理好的 blur 事件,就可能让焦点丢失后无法回到 listbox。











