AI 很容易写出一张看起来整齐的联系表单。但把输入框里的提示字删掉后,还看得出该填什么吗?只用键盘,能找到错误在哪里吗?本练习从这两个问题开始:让 AI 写一个只在本地检查、不发送数据的表单,再用六组操作逐项验收。
适合正在学网页、维护个人站点,或需要检查 AI 生成页面的人。最终得到一份 contact-demo.html;它是教学样例,不是可直接上线的收件系统,也不是无障碍合规认证。
一、准备:一个文本 AI、一份纯文本文件
- 选择你已有访问权限、能够输出代码的 AI 对话工具。只用文本对话,不需要 API 密钥、文件上传或付费插件;免费额度、登录与地区可用性以所用服务为准。
- 准备能保存 UTF-8 纯文本的编辑器和桌面浏览器。本地页面没有后端服务费用。保存时确认扩展名为 .html,不是 .html.txt。
- 全程用本文虚构样例。不要把客户姓名、邮箱、真实留言粘进 AI,也不要给练习添加真实提交地址。
二、先把“可用”写成规则,再让 AI 生成
把下面提示词完整复制过去。它没有要求“做得高级”,而是明确了标签、错误、焦点和不发送数据四组条件。W3C 的标签教程说明,显式关联时,label 的 for 必须与输入控件的 id 一致;placeholder 里的示例不能承担全部标签工作。
请帮我制作一个中文联系表单练习,交付一个可本地打开的完整 HTML 文件。
用途:学习表单标签、错误提示和键盘操作,不上线,不收集真实数据。
字段:称呼、电子邮箱、留言,全部必填。
实现要求:
1. 每个字段有一直可见的 label,for 与唯一 id 完全一致;不要用 placeholder 代替标签。
2. 电子邮箱用 type="email"。必填同时写在可见标签和 required 属性中。
3. 提示与字段用 aria-describedby 关联。发现错误才加 aria-invalid="true",纠正后移除。
4. 点击“检查输入(不发送)”后,显示错误数量和逐字段的纠正方法,聚焦第一个错误字段。
5. 不仅改变颜色,不清空用户已填内容。通过时写明“检查通过,没有发送”。
6. 使用原生控件和清晰的键盘焦点;窄屏不横向溢出。
7. 不调用 fetch/XHR,不引入外部脚本、字体、统计,不连接后端。
8. 按钮使用 type="button";拦截 form 的 submit;加 form-action 'none' 的 CSP 防止误提交。
9. JS 未加载时按钮禁用。不给页面增加上传、注册或支付功能。
输出顺序:完整代码;逐项说明;6个测试用例。
不声称已通过 WCAG 认证,也不要把客户端检查称为安全防护。
测试仅用“小禾”“reader@example.com”“想了解公开课时间。”等虚构内容。
拿到输出后先读代码,不急着双击。搜索 fetch、XMLHttpRequest、外部 script src、action=,确认没有多余联网功能。搜索只是第一道检查,不能代替逐行阅读;不理解的部分让 AI 解释,不要直接接到正式网站。
三、用这份最小版本对照
下面是为本文编写的参考实现。可以直接另存为 contact-demo.html,也可以与 AI 的版本比较。novalidate 让示例统一展示自己的中文提示;required 仍保留字段语义。检查按钮不提交表单,CSP 再限制表单提交,页面没有外部依赖。form-action 的 none 配置见 MDN 文档。
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'none'; base-uri 'none'">
<title>联系表单:仅本地练习</title>
<style>
body { max-width:36rem; margin:2rem auto; padding:0 1rem;
font:1rem/1.6 system-ui; color:#17202a; background:#fff; }
label { display:block; font-weight:bold; margin-top:1rem; }
input,textarea,button { font:inherit; padding:.65rem; }
input,textarea { box-sizing:border-box; width:100%; }
button { margin-top:1rem; }
:focus-visible { outline:3px solid #1455b8; outline-offset:3px; }
.error { color:#9c1b18; min-height:1.6em; margin:.25rem 0; }
[aria-invalid="true"] { border:2px solid #9c1b18; }
</style>
<h1>联系表单练习</h1>
<p>全部必填。只用虚构数据;本页不发送留言。</p>
<p id="summary" role="alert"></p>
<form id="contact" novalidate>
<label for="person">称呼(必填)</label>
<input id="person" autocomplete="name" required
aria-describedby="person-error">
<p id="person-error" class="error"></p>
<label for="email">电子邮箱(必填)</label>
<p id="email-hint">练习可用 reader@example.com。</p>
<input id="email" type="email" autocomplete="email" required
aria-describedby="email-hint email-error">
<p id="email-error" class="error"></p>
<label for="message">留言(必填)</label>
<textarea id="message" rows="4" required
aria-describedby="message-error"></textarea>
<p id="message-error" class="error"></p>
<button id="check" type="button" disabled>检查输入(不发送)</button>
</form>
<p id="result" role="status"></p>
<noscript>本练习需要 JavaScript;按钮保持禁用,不会发送数据。</noscript>
<script>
const byId = id => document.getElementById(id);
const fields = ['person','email','message'].map(byId);
byId('contact').addEventListener('submit', e => e.preventDefault());
byId('check').addEventListener('click', () => {
const errors = [];
byId('result').textContent = '';
fields.forEach(field => {
let text = '';
if (!field.value.trim()) {
text = {person:'请填写称呼。', email:'请填写电子邮箱。',
message:'请填写留言。'}[field.id];
} else if (field.id === 'email' && field.validity.typeMismatch) {
text = '请填写邮箱格式,例如 reader@example.com。';
}
byId(field.id + '-error').textContent = text;
if (text) {
field.setAttribute('aria-invalid','true');
errors.push(field);
} else {
field.removeAttribute('aria-invalid');
}
});
byId('summary').textContent = errors.length
? '有 ' + errors.length + ' 项需要修改,请按字段提示检查。' : '';
if (errors.length) errors[0].focus();
else byId('result').textContent = '输入检查通过;这只是本地演示,没有发送。';
});
byId('check').disabled = false;
</script>
</html>
这里把错误文字挂在对应字段旁,用 aria-describedby 连接;只在检查发现错误后设置 aria-invalid。错误摘要给出数量,光标移到首个错误字段。其依据见 W3C 表单反馈教程和 ARIA21 技术说明。不要把这几项属性当成“加完即合规”的开关,实际阅读顺序和读屏表现仍需检查。
四、按六组操作验收,不只看截图
用浏览器打开文件。先不用鼠标,从页面起点连续按 Tab,再用 Shift+Tab 反向移动。按钮获得焦点后按空格触发检查;本例故意没有“按回车自动提交”的业务行为。
| 操作 | 应看到的结果 | 不通过时先查 |
|---|---|---|
| 全部留空,触发检查 | 提示3项需要修改,焦点回到称呼;每个字段有中文错误 | 错误容器id、字段遍历顺序 |
| 称呼只填空格,另两项正常 | 称呼仍被认为缺失;其他已填内容不丢 | 是否只判断空字符串,漏了trim() |
| 称呼小禾;邮箱reader;留言正常 | 只有邮箱报格式错误,焦点到邮箱 | type=email与typeMismatch检查 |
| 邮箱改为reader@example.com,再检查 | 旧错误消失;显示本地检查通过、没有发送 | 是否清理旧文字及aria-invalid |
| 点击“电子邮箱”标签,再用Tab前后移动 | 标签能把焦点带到邮箱;焦点轮廓清晰 | for/id是否匹配,是否去掉outline |
| 缩窄到约360像素,并放大页面文字 | 控件仍在内容区域内,文字不被裁掉 | 固定宽度、box-sizing、过小字号 |
再打开浏览器开发者工具的网络面板:清空记录,只触发一次检查,确认页面没有新增数据请求。代码审查和网络观察要一起做,不能仅凭“屏幕显示成功”断定没有发送。若使用读屏软件,还应听字段名称、必填状态和错误说明;不同浏览器与读屏组合需要实际测试,本文不冒称已经覆盖。
五、常见返工:缩小修改范围
- 只有红框没有文字:要求 AI 加入具体纠正方法,例如“请填写邮箱格式”,不要只写“格式错误”。
- 每打一字都跳走焦点:要求只在点击检查后移动焦点,输入过程中不要抢焦点。
- 改对了还提示错误:重新检查时先更新每个字段的错误文字和属性,再计算错误总数。
- AI 把按钮改成“发送成功”:改回“检查通过,没有发送”。真实发送需要后端、服务器校验、隐私说明等独立工作,本练习没有实现。
邮箱格式正确,也不代表这个邮箱存在或归填写者所有;MDN 的 email 输入文档明确区分这两件事。W3C 的验证教程同样提醒:客户端检查不能替代服务器端验证。
最终验收标准很具体:三个标签对应正确、错误能用文字理解、键盘能走完整条路径、纠正后状态更新、没有实际发送。若你接下来要改颜色,可继续看 文字和底色逐对检查的配色练习,不要只凭配色看起来舒服就取消焦点与错误提示。
资料核对:2026年10月29日。本文样例为原创教学代码;实际站点上线前需要结合真实业务与辅助技术测试。