原生 select 不可替代,因其唯一支持键盘导航、屏幕阅读器识别、表单提交和移动端系统级选择器;自定义 div+ul 下拉必卡可访问性,根源在于语义缺失导致浏览器无法接管交互逻辑。

原生 select 是唯一能同时满足键盘导航、屏幕阅读器识别、表单提交和移动端系统级选择器调用的方案——不是“够用”,而是“不可替代”。所有自定义下拉菜单(div+ul)在重构期强行修补,最终都会卡死在焦点管理、AR状态同步或iOS失焦上。
为什么手写 div+ul 下拉菜单必卡可访问性
这不是 JS 写得不够细的问题,是语义缺失导致浏览器无法接管基础交互逻辑:
- Tab 进入后方向键失效、
Enter无响应:缺少role="combobox"和aria-activedescendant同步机制 - 屏幕阅读器读不出“已展开”“已选中”:
aria-expanded和aria-selected没随点击/键盘操作动态更新 - iOS Safari 快速切换时闪退或自动失焦:非原生控件触发
focus()后,系统级选择器不接管,DOM 焦点与渲染层脱节
用原生 select 替换时必须守住的嵌套底线
浏览器会“修复”错误嵌套,但结果往往是选项消失或生成两个独立下拉框:
- 只允许两种直接子元素:
option和optgroup -
optgroup必须包含至少一个option,且不能嵌套其他optgroup - 禁止在
select内写div、span、ul等非标准子节点 - 分组标题用
optgroup label="硬件",不要加disabled或试图让它可点击
错误示例:<select><div><option>A</option></div></select> → 浏览器解析为分离的 <select></select> 和 <div><option>A</option></div>,选项彻底丢失。
立即学习“前端免费学习笔记(深入)”;
保留设计师 UI 需求的最小侵入式改造法
不用推翻重写,两步即可兼顾可访问性与视觉定制:
- 把原生
select设为visually-hidden(用 CSS 隐藏但保留可访问性),例如:.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } - 用
button+ul构建视觉层,通过 JS 同步select.value和select.selectedIndex,并手动触发select.dispatchEvent(new Event('change', { bubbles: true }))
关键点在于:所有用户行为(键盘、鼠标、屏幕阅读器)都作用于原生 select,视觉层只是镜像;否则,任何“模拟”都会在某个边界场景崩掉——比如连续快速按两次 ArrowDown 后焦点跳到页面顶部。
第三方插件或自定义组件里最容易被忽略的硬伤
很多团队花时间调样式、加搜索、优化滚动,却漏掉三个致命点:
- 没给触发按钮加
aria-haspopup="listbox"和aria-expanded,屏幕阅读器根本不知道这是个下拉 - 选项列表没设
role="listbox",每个li没设role="option",AR 不识别为可选项目 - 打开菜单后没用
element.focus()把焦点移入第一项,导致键盘用户必须按多次Tab才能进入选项
这些不是锦上添花,是让屏幕阅读器用户第一次操作就能听懂“我在哪、能干嘛”的最低门槛。一旦漏掉,后续所有交互优化都建立在流沙之上。



















