用 aria-live 与 aria-describedby 实现异步表单校验错误播报
【摘要】 异步表单校验常出现错误提示已显示但读屏器未感知、重复提交重复播报的问题。本文用 role=alert 汇总错误、aria-describedby 关联字段错误,并在聚焦首个错误字段时通过消息去重避免重复播报,附最小可运行示例。
用 aria-live 与 aria-describedby 实现异步表单校验错误播报
问题场景
在注册、结算等表单中,校验常放在提交后异步完成。若错误提示只是把红色文字插入页面,屏幕阅读器用户可能不知道提交失败;若每个字段错误都使用独立的 live region,又可能连续打断播报。更常见的问题是:同一份错误连续提交两次,提示区域被重复写入,读屏器再次播报,造成干扰。
目标可以拆成四点:字段错误与输入框建立稳定的描述关系;提交后通过一个 role=alert 区域播报汇总;把焦点移到首个错误字段;错误集合没有变化时,不重复更新播报区域。
设计要点
- 表单使用 novalidate,由脚本统一接管校验和错误展示。
- 每个输入框用 aria-describedby 指向自己的错误文本节点。错误文本出现时再移除 hidden,并给输入框设置 aria-invalid='true';修正后移除该属性。
- 页面只保留一个 role='alert' 的汇总区域。role=alert 隐含 assertive 播报,代码中仍写出 aria-live='assertive' 和 aria-atomic='true',便于阅读意图。
- 异步结果返回后,先写入字段错误,再聚焦首个错误字段。这样聚焦时 aria-describedby 指向的错误文本已经可见。
- 用 lastAnnouncement 记录上一次写入的播报文本。只有文本变化时才修改 live region;连续提交相同错误不重复写 DOM。
最小可运行示例
保存为一个 HTML 文件即可运行。异步校验用 setTimeout 模拟,真实项目中替换为 fetch 或接口 SDK,并保留同样的错误对象结构。
<!doctype html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<title>异步表单校验无障碍示例</title>
</head>
<body>
<form id='signup' novalidate>
<p>
<label for='name'>姓名</label><br>
<input id='name' name='name' autocomplete='name'
aria-describedby='name-error'>
<span id='name-error' hidden></span>
</p>
<p>
<label for='email'>邮箱</label><br>
<input id='email' name='email' type='email' autocomplete='email'
aria-describedby='email-error'>
<span id='email-error' hidden></span>
</p>
<button type='submit'>提交</button>
<div id='form-status'
role='alert'
aria-live='assertive'
aria-atomic='true'></div>
</form>
<script>
const form = document.getElementById('signup');
const statusEl = document.getElementById('form-status');
let lastAnnouncement = '';
function setFieldError(input, message) {
const errorEl = document.getElementById(
input.getAttribute('aria-describedby')
);
if (message) {
errorEl.textContent = message;
errorEl.hidden = false;
input.setAttribute('aria-invalid', 'true');
} else {
errorEl.textContent = '';
errorEl.hidden = true;
input.removeAttribute('aria-invalid');
}
}
// 模拟异步校验。真实项目替换此函数,返回 { 字段名: 错误文本 }。
function validate(values) {
return new Promise((resolve) => {
setTimeout(() => {
const errors = {};
if (!values.name.trim()) {
errors.name = '请输入姓名';
}
if (!values.email.trim()) {
errors.email = '请输入邮箱';
} else if (!values.email.includes('@')) {
errors.email = '邮箱格式不正确';
}
resolve(errors);
}, 400);
});
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const inputs = Array.from(form.querySelectorAll('input'));
// 清空上一轮字段错误;不要清空 statusEl,避免额外播报。
inputs.forEach((input) => setFieldError(input, ''));
const values = Object.fromEntries(new FormData(form).entries());
const errors = await validate(values);
inputs.forEach((input) => {
setFieldError(input, errors[input.name] || '');
});
const firstInvalid = inputs.find((input) => errors[input.name]);
const summary = firstInvalid
? '表单校验未通过:' + Object.values(errors).join(';')
: '提交成功';
// 相同播报文本不重复写入,避免连续提交同一错误时反复播报。
if (summary !== lastAnnouncement) {
statusEl.textContent = summary;
lastAnnouncement = summary;
}
if (firstInvalid) {
// 先展示错误,再聚焦,确保描述文本可被读取。
firstInvalid.focus();
}
});
</script>
</body>
</html>
运行与验证
把示例保存为 accessible-form.html,用浏览器打开。直接按 Tab 找到提交按钮并回车,或清空字段后提交,会看到焦点落到首个错误输入框。使用支持 ARIA 的读屏器测试时,重点检查三点:错误文本是否在聚焦时被读出;汇总区域是否只在错误集合变化时播报;修正字段后再次提交,旧错误是否消失。
真实接入时,把 validate 中的 setTimeout 换成自己的异步请求,并保持返回结构为字段名到错误文本的映射。若接口返回错误列表,可以先转换成该结构,再复用 setFieldError 和播报逻辑。
边界与限制
- role=alert 是 assertive 播报,适合提交失败、提交成功这类重要状态,不适合输入过程中的每次提示。输入过程中的提示可改用 aria-live='polite',或只更新字段级描述。
- 不同读屏器对 alert 与聚焦时 aria-describedby 的处理不同,可能出现汇总与字段错误各读一次。若重复感明显,可把汇总缩短为「表单有 2 个错误」,具体错误交给字段描述;是否需要保留完整错误列表,应结合测试决定。
- 示例中的邮箱检查只判断 @,是占位逻辑,不能替代服务端校验。真实规则应返回明确的字段错误,不要把多个错误拼在一个字符串里。
- 快速连续提交可能造成异步结果乱序。生产代码应禁用提交按钮、增加请求序号,或只接受最后一次请求的结果。
- aria-describedby 可以指向多个 id,例如提示文本和错误文本同时存在时写成两个 id;不要用颜色作为唯一错误提示,也不要移除输入框的焦点轮廓。
按照上述结构,字段错误有可见文本和描述关系,汇总播报受去重控制,焦点顺序也符合键盘用户预期。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)