label 与 select 的 for/id 必须严格匹配(大小写、拼写、空格均需一致),且 DOM 中相邻;禁用 display: none 隐藏 select;option 首项应设 value="" disabled selected;change 事件须直接绑定 select 元素。
屏幕阅读器靠 for 属性精准定位到 select,一旦大小写、空格或拼写有差,关联就断了。比如 label for="City" 对应 select id="city",iOS VoiceOver 就不会聚焦。
常见错误:
label 嵌套 select —— 虽然能工作,但 DOM 结构受限,后续加按钮或图标容易破坏语义aria-label 替代 label —— 表单提交时焦点行为异常,且部分读屏软件不触发原生聚焦逻辑id —— 浏览器只认第一个,其余全失效像 <option value="">请选择</option> 这种写法,屏幕阅读器会把它当有效选项读出来,用户按回车就“选中”了空值,后端收不到合法数据。
正确做法是同时设三个属性:
立即学习“前端免费学习笔记(深入)”;
value="" —— 确保提交时不带值disabled —— 让它不可选(注意:Safari 有时不自动灰显,建议补 opacity: 0.6)selected —— 页面加载时默认高亮,但不参与提交示例:<option value="" disabled selected>-- 请选择 --</option>
想“美化”下拉框,直接 display: none 或 visibility: hidden 掉 select,等于主动放弃无障碍支持——屏幕阅读器根本看不到这个控件。
如果必须自定义样式,只能这样操作:
select 在 DOM 中(不可移除、不可隐藏)opacity: 0 + position: absolute 覆盖在自定义 UI 上方ArrowDown、Enter)仍可触发aria-expanded 和 aria-controls,并透传关键事件很多人把事件监听写在父容器或用 click / input,结果移动端点不触发、Enter 键没反应、或者重复执行两次。
记住两点:
select 只发 change 事件,不发 input
select 节点,不是它的 label 或外层 div
onchange="handle()" 并立即执行——页面加载时就会调一次,除非加防抖推荐写法:select.addEventListener('change', handler)
真实场景里最容易被忽略的,是 iOS VoiceOver 要求 label 和 select 在 DOM 中相邻——中间插个 span 或换行符,点击 label 文本就可能失焦。这不是 bug,是规范强制要求。