表单控件必须通过 label 或 aria-labelledby 关联文本,错误提示需用 aria-describedby 绑定,禁用控件应保留语义并确保可发现,自定义按钮须补全 role、tabindex 和键盘事件支持。

表单控件没加 label 或错误提示没绑定 aria-describedby,屏幕阅读器大概率只报“编辑框”“复选框”,完全不知道要填什么、错在哪——这不是兼容性问题,是语义缺失。
label 没关联上,读屏器就认不出字段用途
浏览器不会自动把旁边的文字当成输入框的标签,哪怕视觉上挨得很近。常见错误是只写 placeholder 或纯靠 CSS 布局排版。
- 首选方案:用显式
for/id关联,<label for="email">邮箱地址</label><input id="email"> - 嵌套写法可行但有坑:
<label>邮箱地址<input type="email"></label>,但内部不能塞<div> 或其他块级元素,否则 NVDA 可能截断朗读 <li>禁用 <code>placeholder="邮箱"单独充当 label——它不被所有读屏器识别,且一输入就消失,无法持续提供上下文 - 如果已有可见文本(比如图标旁写了“密码”),优先用
aria-labelledby="id-of-text",避免重复描述 - 出错时设
aria-invalid="true";校验通过后必须同时移除该属性,不能只清空值或留空 - 错误文案容器要有唯一
id,并通过aria-describedby="error-email"显式指向它;不能依赖 class 名或兄弟节点位置 - 错误容器别用
display: none隐藏,否则读屏器直接跳过;改用visibility: hidden或opacity: 0+aria-hidden="true" - 若错误文案是动态插入的(如 AJAX 校验返回),确保插入后 DOM 中仍存在该
id,且aria-describedby值未被覆盖 - 原生
disabled已隐含aria-disabled="true",无需手动添加——重复加反而可能干扰部分读屏器 - 禁用的
<input>、<button></button>、<input type="checkbox">默认不参与 Tab 导航,这是规范行为,不要用tabindex="0"强行拉进来 - 若需“可聚焦但只读”,改用
readonly+ 视觉样式标明状态,比如灰底+斜线纹,而非禁用色 - 测试时发现禁用控件“完全读不到”,先查三件事:有没有
label for="xxx"、id是否拼错、父容器是否误加了aria-hidden="true" - 最省事也最安全:直接用
<button type="button">提交</button>,语义、焦点、键盘事件(Enter/Space)全部原生支持 - 若必须用自定义元素(如图标按钮),得加
role="button"+tabindex="0",再手动监听keydown,对 Enter 和 Space 都触发相同逻辑 - 纯
<svg></svg>按钮必须配aria-label或aria-labelledby,否则读屏器可能只读“图形”或直接跳过 - 别在
onclick里只处理鼠标,却忽略键盘路径——这等于默认放弃 15% 以上依赖键盘的用户
错误提示必须和输入框建立可访问绑定
仅靠红边框、Toast 弹窗或 DOM 位置相邻,屏幕阅读器根本感知不到错误。用户输完直接按回车,却听不到任何反馈,这是典型的无障碍断裂点。
禁用控件不是“不可见”,而是“不可聚焦但需可发现”
disabled 是合法且推荐的状态,不是 bug。它的语义就是“当前不可操作”,屏幕阅读器会主动播报“已禁用”,前提是标签存在、ID 正确、没被父级 aria-hidden="true" 意外屏蔽。
按钮类交互必须语义化或补全键盘支持
一个 <div onclick="submit()">提交</div> 对鼠标用户没问题,但键盘用户 Tab 不进去、Enter 按了没反应、读屏器甚至不把它当按钮——这不是体验差,是直接不可用。
真正难的不是堆 ARIA 属性,而是保持语义链完整:label → input → error → state,每环都得有 ID、有绑定、无遮挡。一旦其中一环断开(比如删了 ID 却忘了同步更新 aria-labelledby),整个可访问性就塌了一角。











