应优先使用原生<select>而非自定义下拉,因其被所有主流屏幕阅读器一致支持、自带键盘导航与语义角色、高对比度模式适配;多选场景若VoiceOver体验差,建议改用fieldset包裹的复选框组。

用 <select> 而不是自定义下拉,除非你真有理由
原生 <select> 是目前唯一被所有主流屏幕阅读器(NVDA、JAWS、VoiceOver)一致支持、无需额外 ARIA 修补的下拉控件。它自带键盘导航(↑/↓ 切换选项、Enter 确认、Esc 关闭)、焦点管理、语义角色(combobox + listbox 自动绑定),且在 macOS 和 Windows 高对比度模式下默认适配。
常见错误是用 <div> + <ul> + JavaScript 模拟下拉,再手动加一堆 aria-expanded、aria-haspopup、role="listbox" —— 一旦漏掉 aria-activedescendant 或没处理好 Tab 键逻辑,就会导致 VoiceOver 用户卡死在按钮上无法进入列表。
- 必须用
<label for="id">或包裹式<label><select></select></label>显式关联标签 - 禁用
display: none或visibility: hidden隐藏选项;要用hidden属性或aria-hidden="true"(仅对视觉隐藏但保留语义) - 不要给
<option>加tabindex:它本就不应获得独立焦点
<select multiple> 的可访问陷阱与绕过方案
多选 <select> 在 Windows + NVDA 下基本可用,但在 macOS + VoiceOver 中存在严重问题:用户无法用方向键逐项切换已选状态,只能靠 Ctrl+Option+Space 切换,且无视觉反馈提示当前聚焦项是否已被选中。
如果业务强依赖多选且需高保真体验,建议降级为一组带 aria-labelledby 的复选框(<input type="checkbox">),并用 fieldset + legend 包裹,确保语义分组清晰:
立即学习“前端免费学习笔记(深入)”;
<fieldset aria-labelledby="multi-label"> <legend id="multi-label">请选择偏好</legend> <input type="checkbox" id="pref1" name="pref" value="a"> <label for="pref1">邮件通知</label> </fieldset>
- 避免用
role="group"替代fieldset:后者自带隐式role="group"且被所有读屏器识别 - 若坚持用
<select multiple>,至少确保size属性设为 ≥3,让用户可见多个选项,减少盲操作
禁用状态、空选项与占位符的正确写法
禁用整个下拉时,用 disabled 属性即可,屏幕阅读器会自动播报“已禁用”。但禁用单个 <option> 时,仅加 disabled 不够 —— 它仍会被读出,且可能被键盘意外聚焦(尤其在旧版 IE/Edge)。
更稳妥的做法是:用 aria-disabled="true" + tabindex="-1" + CSS 视觉灰化,并在 <option> 文本前加括号注明(如 <option value="" aria-disabled="true" tabindex="-1">(不可用) 企业版</option>)。
- 空选项(占位符)必须用
<option value="" disabled selected>请选择...</option>,不能只靠placeholder(<select>不支持该属性) - 禁用项不要设
value,否则表单提交时可能意外带上空字符串 - Chrome 120+ 开始支持
:has(:disabled),可用于精准样式控制,但 Safari 尚未支持,慎用
键盘交互细节决定是否真可用
原生 <select> 的键盘行为看似简单,但实际测试中常发现:用户按字母键想快速跳转到以该字母开头的选项,却失败 —— 原因是选项文本含空格、标点,或首字符为数字/符号。此时需确保 <option> 的文本内容以字母开头,且无前置不可见字符(如 零宽空格)。
- 长选项文本建议截断后加
title属性(<option title="完整描述">简写</option>),但注意 VoiceOver 默认不读title,需配合aria-label - 避免在
<select>上监听keydown并preventDefault():这会直接破坏原生导航逻辑 - 移动端 Safari 对
<select>的focus事件触发时机异常,如有 JS 初始化逻辑,改用DOMContentLoaded或load事件更可靠
最易被忽略的是:当页面动态增删 <option> 后,未重置 selectedIndex,导致焦点悬空或默认值错乱。每次 DOM 更新后,显式设置 selectElement.selectedIndex = 0(或根据业务逻辑赋值)是必要步骤。



















