真正的实时报错无障碍提示需四者同步:label显式含必填/格式说明、aria-invalid="true"标识错误状态、aria-describedby关联动态错误文本(用visually-hidden类)、js实时更新并重置焦点;缺一不可。

仅靠 required、type="email" 或 :invalid 伪类,无法实现真正的实时报错无障碍提示——屏幕阅读器根本不会主动播报错误,用户也看不到、听不到、摸不到哪里错了。
label 必须显式包含“必填”或格式说明
浏览器原生校验只在提交时触发,且错误弹窗(如 “Please fill out this field”)不读 label 文本。如果 label 写的是 “邮箱”,屏幕阅读器就只读 “邮箱”,不会加 “(必填)” 或 “应为邮箱格式”。
- ✅ 正确做法:
<label for="email">邮箱(必填,例如 name@example.com)</label><input id="email" required type="email"> - ❌ 错误做法:
<label for="email">邮箱</label>+ 单独加required和type="email"—— 语义缺失,读屏无提示 - ⚠️ 注意:若设计禁止修改文案,可用
aria-label补充,但必须保留字段名,例如aria-label="邮箱,必填,格式为 xxx@xxx.xxx";不能只写aria-label="必填",否则读屏会跳过字段名
错误状态必须同步设置 aria-invalid="true" 并关联描述
:invalid 伪类能控制样式(如红框),但对屏幕阅读器完全静音。没有 aria-invalid="true",读屏仍认为字段“正常”,哪怕它已高亮报错。
- ✅ 必须组合使用:
<input id="phone" aria-invalid="false" aria-describedby="phone-error"><div id="phone-error" class="visually-hidden"></div> - ✅ JS 实时校验失败时,立刻:
input.setAttribute('aria-invalid', 'true'),并更新phone-error.textContent = "请输入11位手机号" - ⚠️ 注意:
aria-describedby的 ID 必须与目标<div> 的 <code>id完全一致(大小写敏感);该<div> 不能用 <code>display: none隐藏,要用visually-hidden类(如position: absolute; clip: rect(0 0 0 0);)确保可读不可见移动端软键盘遮挡错误提示是高频失能点
在手机上,把错误提示放在输入框下方等于“主动藏起来”——软键盘一弹出,提示直接被盖住。用户反复输错却毫无反馈,不是逻辑问题,是信息不可见。
- ✅ 推荐位置:错误提示紧贴输入框上方,或使用
position: fixed浮层 +aria-live="polite"区域播报(需配合aria-atomic="true") - ✅ 补充反馈:除文字外,同步改变边框颜色、添加图标(带
aria-hidden="true")、轻微震动(navigator.vibrate)等多通道提示 - ⚠️ 注意:不要依赖
title或placeholder显示错误——它们不被多数读屏稳定读出,且无法在焦点状态下持续呈现
动态表单(如多步、异步加载)必须重置焦点和 live 区域
切换步骤后焦点丢失、验证码刷新后提示未更新、JS 注入新字段但没补
for/id关联——这些不是边缘 case,而是无障碍崩溃的常见起点。- ✅ 步骤切换后,立即
stepHeading.focus()或firstInput.focus(),且确保该元素不在aria-hidden="true"容器内 - ✅ 每次验证码刷新,必须更新
aria-describedby指向的新id,并重设其textContent;避免复用旧 DOM 节点导致读屏缓存旧值 - ⚠️ 注意:用
display: none切换步骤时,隐藏区域的元素仍在 tab 顺序中,会导致键盘用户卡在空白处;优先用hidden属性或移除 DOM
最常被忽略的其实是节奏感:label 文案、ARIA 属性、CSS 状态、JS 更新必须四者严格同步。差一个环节,对视障用户来说就是“字段存在但不可知、错误发生但不可闻、修正完成但无确认”。
- ✅ 推荐位置:错误提示紧贴输入框上方,或使用











