原生 select 是唯一能同时满足键盘导航、屏幕阅读器识别、表单提交和移动端系统级选择器调用的下拉方案;手写 div+ul 的“伪下拉”因语义缺失导致浏览器无法接管基础交互,可访问性存在硬伤。

原生 select 是唯一能同时满足键盘导航、屏幕阅读器识别、表单提交和移动端系统级选择器调用的下拉方案——手写 div+ul 的“伪下拉”在可访问性上是硬伤,不是细节没写好,而是语义缺失导致浏览器无法接管基础交互。
为什么隐藏菜单后屏幕阅读器读不出选项
常见错误是给整个下拉容器加 aria-hidden="true" 或用 display: none / visibility: hidden 隐藏未展开状态。这些方式会直接从可访问树中移除内容,屏幕阅读器根本感知不到选项存在。
- 正确做法:菜单关闭时保持 DOM 存在,仅用视觉隐藏(如
opacity: 0+pointer-events: none),或用hidden属性(它不移除可访问性) - 若必须用
display: none,需在展开前动态移除该样式,并确保aria-expanded="true"和aria-hidden="false"同步更新 -
aria-hidden="true"只能加在纯装饰性子节点上(比如图标svg),绝不能加在包含role="option"的父容器上
自定义下拉必须写的 ARIA 属性组合
用 button+ul 实现时,仅靠 class 和 CSS 不足以让屏幕阅读器理解这是“下拉”,必须显式声明角色与状态:
- 触发按钮:
role="combobox"+aria-haspopup="listbox"+aria-expanded="false"+aria-controls="id-of-list" - 选项列表:
role="listbox"+id="id-of-list"(与aria-controls值一致) - 每个选项:
role="option"+aria-selected="true/false"+ 唯一id,并由触发按钮的aria-activedescendant动态指向当前高亮项 - 禁止在
ul上写role="menu"—— menu 是右键上下文菜单,语义错位会导致读屏误读为“快捷键操作”
键盘焦点卡在触发按钮出不去的修复点
用户按 Tab 进入下拉触发器后,方向键无法进入选项、Esc 关闭后焦点没回退——这不是 JS 事件没绑,而是焦点流设计断裂:
- 菜单展开时,
button必须设aria-expanded="true",且立即用ul.querySelector('[role="option"]').focus()将焦点移入第一个选项 - 方向键遍历时,必须手动管理
aria-activedescendant,并同步更新aria-selected(注意:不要用click()模拟选中,要发change事件) - Esc 关闭菜单后,焦点必须回到触发按钮,并重置
aria-expanded="false";若按钮本身不可聚焦(比如用了div),立刻失败 - 所有可聚焦元素(包括选项)必须有明确
tabindex:触发按钮用tabindex="0",选项用tabindex="-1"(只允许键盘导航进入,不参与 Tab 顺序)
真正难的不是写对 ARIA,而是让所有状态(aria-expanded、aria-selected、aria-activedescendant、焦点位置、视觉高亮)始终严格同步——差一个就导致屏幕阅读器播报错乱或键盘用户“掉线”。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











