根本原因是html结构未保障多端语义一致性,具体表现为label与input的for/id关联不严格、select被隐藏或劫持、aria状态未动态同步、以及动态渲染后焦点逻辑未重置。

表单在移动端或智能电视上失焦、点不中、读不出,根本原因不是“设备特殊”,而是 HTML 结构没扛住多端语义一致性。 常见的“PC 能用,手机 VoiceOver 就跳过 label”“遥控器无法选中下拉框”,基本都出在 label 关联方式、tabindex 干预、或原生控件被隐藏/劫持这几个环节。
label 和 input 的 for/id 关联必须严格相邻且大小写敏感
移动端(尤其是 iOS VoiceOver)对 label 与 input 的 DOM 位置和属性匹配极其敏感。显式关联是唯一跨设备可靠的方案。
-
for属性值必须与input的id完全一致:包括大小写、连字符、下划线,且首尾不能有空格或不可见字符(如模板渲染时插入的换行) - 避免把
label和input拆开在不同容器里,比如中间插了<p></p>或<div class="wrapper"> —— iOS 下点击 label 文本可能完全不聚焦<li>禁用隐式包裹写法:<code><label>姓名<input name="name"></label>,DOM 动态插入注释或换行极易断开语义链 - 对于
<select></select>,label必须紧邻其前或后,且<option></option>占位项不能是纯空格或仅含空格的value="",应设为<option value="" disabled selected>-- 请选择 --</option> - 禁止用
display: none或visibility: hidden隐藏原生<select></select>—— 它必须保留在可聚焦流中 - 自定义下拉 UI 必须透传关键键盘事件:
ArrowDown/ArrowUp、Enter、Space,并手动调用select.focus()或option.click() - 若必须替换 UI,需用
role="combobox"+aria-expanded+aria-controls重建语义,并确保<select></select>仍存在于 DOM 中、tabindex="-1"仅用于视觉隐藏,不脱离焦点流 - iOS Safari 对
<select></select>的focus()行为有延迟,建议在用户点击 label 后加setTimeout(() => select.focus(), 0)触发 - 错误文案必须有稳定
id,例如<div id="err-username">手机号格式不对</div>,并在input上设置aria-describedby="err-username" -
aria-invalid="true"不能提前写死,必须在用户输入后校验失败时才设;提交前清空,否则会误导辅助技术认为字段始终无效 - 多个描述(如格式说明 + 错误)用空格分隔:
aria-describedby="desc-username err-username",不要塞进aria-label—— 后者会覆盖label文本,字段名直接消失 - 验证失败后,应调用
input.focus()并确保错误区域在视口内;部分 Android TalkBack 不播报未聚焦元素的aria-live,所以聚焦本身是触发播报的关键
select 下拉框在遥控器/触屏上失效的常见原因
智能电视遥控器本质是键盘导航设备(方向键 + OK),而很多“美化下拉”方案直接 display: none 掉原生 <select></select>,导致焦点丢失、事件中断、ARIA 状态不同步。
aria-describedby 和 aria-invalid 在动态验证中容易失效
移动端或屏幕阅读器不会主动监听 DOM 变化,只依赖初始结构和明确的状态变更。靠 CSS 显示/隐藏错误提示,或仅改 innerHTML,读屏软件大概率读不到。
最常被忽略的一点:无障碍不是“加几个 ARIA 属性就完事”。当 <select></select> 被封装进 Web Component、或表单由 JS 动态渲染时,id 和 for 的同步、tabindex 的重置、以及焦点恢复逻辑,都得在每次 DOM 更新后重新确认 —— 这些细节在 PC 上可能“凑合能用”,但在遥控器或 VoiceOver 下,差一个空格、一次未清理的 tabindex="0",整个表单就不可操作了。











