表单可访问性需从结构层用对label、fieldset、legend;label必须严格配对for/id;fieldset+legend是分组唯一可靠方式;aria-invalid需手动同步校验状态。

表单可访问性不是靠加一堆 ARIA 补丁堆出来的,而是从结构层就该用对 label、fieldset、legend 这三样原生标签;骨架塌了,再 fancy 的 JS 也救不回屏幕阅读器用户的填写体验。
label 和 for/id 必须严格配对,大小写、空格、JS 动态改 ID 都会断开绑定
常见错误现象:点击文字不聚焦输入框、VoiceOver 完全读不出字段含义、自动化测试报“missing label”。
- for 和 id 值必须完全一致(含大小写、连字符、下划线),比如
id="email_address"就得配for="email_address",不能是for="emailAddress" - React/Vue 中别硬写字符串,用变量或
htmlFor={id}/:html-for="id"保证同步 - 避免
<label>邮箱<input></label>这种隐式包裹——DOM 插入注释、换行或 JS 移动节点时容易失效,iOS Safari 和旧版 TalkBack 支持不稳定 - 在 Chrome DevTools 的 Accessibility 面板里检查
label的 “Name” 是否可读、是否指向正确控件
fieldset + legend 是分组逻辑的唯一可靠表达
使用场景:单选按钮组(如性别)、复选框组(如兴趣偏好)、地址字段块、多步骤设置项。仅靠 CSS 排版或 <div class="group"> 包裹,屏幕阅读器只会平铺读出所有选项,完全丢失上下文。
<ul>
<li>
<code><legend></legend> 必须存在且为纯文本,不能只放图标(如 <svg></svg>);若需视觉隐藏,用 clip-path: inset(100%) 或绝对定位,但 DOM 结构不能丢
fieldset:NVDA 和 JAWS 可能跳过中间层级,导致用户听不到“这是第几组”role="group" + <div> 模拟——语义强度远不如原生,部分辅助技术根本不识别
<li>移动端尤其依赖这个结构:VoiceOver 在“组模式”下会把整组当一个单元滑动,没有 <code>legend 就等于没提供上下文required 不触发实时反馈,aria-invalid 必须手动同步
常见错误现象:“输错邮箱格式没提示”“密码太短却没朗读错误”“提交失败后焦点没落到第一个错位”。required 只拦提交,不告诉用户“现在就错了”。
- JS 校验失败时,必须设
aria-invalid="true";校验通过后,必须设回aria-invalid="false" - 错误文案要具体,比如
"邮箱缺少 @ 符号",而不是"格式错误" - 用
aria-errormessage指向错误提示元素的id,确保辅助技术可关联(例如<input aria-errormessage="email-error"><div id="email-error">...</div>) - 提交失败后,调用
input.focus()聚焦第一个aria-invalid="true"的字段
type 和 autocomplete 决定键盘、校验、自动填充能否生效
全用 type="text" 等于放弃浏览器能力:iOS 不弹数字键盘、Chrome 拒绝保存密码、邮箱格式无基础校验、自动填充失效。
- 邮箱:必须
type="email"+autocomplete="email" - 密码:用
type="password"+autocomplete="current-password"(新密码用"new-password") - 手机号:用
type="tel",iOS 自动弹数字键盘 - 地址字段:优先用标准
autocomplete值,如"street-address"、"postal-code"、"country" - 注意:
type="number"的值仍是字符串,且允许粘贴e、-等非法字符;提交前必须做额外校验
最常被忽略的复杂点是:JS 动态生成表单时,id 和 for 的同步、aria-invalid 的状态管理、以及 legend 的 DOM 存在性——这些地方一松懈,可访问性就直接掉档。别指望测试工具扫一遍就完事,得用 VoiceOver + 键盘真机走一遍流程。











