HTML 表单
这一课你将能做到
- 构建一个完整、可访问的登录表单,包含邮箱、密码、记住我等常用字段
- 正确使用
<label>、for属性、name属性和输入类型,确保表单对键盘和屏幕阅读器友好 - 理解表单验证的基础机制,包括 HTML5 内置验证和 ARIA 错误提示模式
为什么要学
表单是 Web 与用户交互的核心桥梁:注册、登录、搜索、评论、支付都离不开它。CodeForge 的登录页、课程评价、搜索栏都是表单。一个设计良好的表单不仅看起来整洁,更要对所有人可用——包括只用键盘操作的用户、依赖屏幕阅读器的用户、以及使用自动填充密码管理器的用户。
初学者常犯的错误是把表单当成「几个输入框加一个按钮」随意堆砌。实际上,表单涉及标签关联、输入类型选择、验证反馈、提交行为、安全策略等多个维度。基础不牢,后面接后端 API 或 React/Vue 表单库时会反复踩坑。
先建立图像
一个标准的登录表单结构:
<form action="/login" method="POST" novalidate>
├── <label for="email">邮箱</label>
├── <input type="email" id="email" name="email" required>
│
├── <label for="password">密码</label>
├── <input type="password" id="password" name="password" required minlength="8">
│
├── <input type="checkbox" id="remember" name="remember">
├── <label for="remember">记住我</label>
│
└── <button type="submit">登录</button>
跟着做
步骤 1:构建 CodeForge 登录表单
创建 forms.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 | CodeForge</title>
<style>
:root { --primary: #2563eb; --surface: #fff; --text: #0f172a; --muted: #64748b; }
body { font-family: system-ui, sans-serif; background: #f1f5f9; display: grid; place-items: center; min-height: 100vh; margin: 0; }
.form-card { background: var(--surface); padding: 2rem; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); width: min(100% - 2rem, 360px); }
.form-group { margin-bottom: 1.25rem; }
label { display: block; font-size: 0.875rem; font-weight: 500; color: var(--text); margin-bottom: 0.375rem; }
input[type="email"], input[type="password"] { width: 100%; padding: 0.625rem 0.875rem; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; box-sizing: border-box; }
input:focus { outline: 2px solid var(--primary); outline-offset: 2px; }
.hint { font-size: 0.75rem; color: var(--muted); margin-top: 0.25rem; }
.checkbox-group { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1.25rem; }
.checkbox-group label { margin: 0; font-weight: 400; }
button[type="submit"] { width: 100%; padding: 0.75rem; background: var(--primary); color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 500; cursor: pointer; }
button:focus { outline: 2px solid var(--primary); outline-offset: 2px; }
</style>
</head>
<body>
<main>
<div class="form-card">
<h1 style="margin-top:0;font-size:1.25rem;">登录 CodeForge</h1>
<form action="/login" method="POST" novalidate>
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" placeholder="name@example.com" autocomplete="email" required aria-required="true" aria-describedby="email-hint">
<p class="hint" id="email-hint">我们永远不会分享你的邮箱。</p>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" autocomplete="current-password" required minlength="8" aria-required="true" aria-describedby="password-hint">
<p class="hint" id="password-hint">至少 8 位字符。</p>
</div>
<div class="checkbox-group">
<input type="checkbox" id="remember" name="remember" value="1">
<label for="remember">记住我(30 天)</label>
</div>
<button type="submit">登录</button>
</form>
</div>
</main>
</body>
</html>
保存后在浏览器打开:不填内容点登录(浏览器阻止提交并提示);按 Tab 切换字段(验证键盘可访问性);检查 label 的 for 与 input 的 id 是否一一对应。
步骤 2:理解 name 属性的作用
name 是表单数据提交时的键名。当用户填写邮箱并点击提交,浏览器向 /login 发送的请求体为:
email=name%40example.com&password=secret123&remember=1
没有 name 的字段不会被提交。id 用于页面内关联(如 <label for>、JS 选择),name 用于服务端接收数据。两者职责不同,通常同名但互不替代。
步骤 3:HTML5 内置验证属性
<!-- 必填 -->
<input required>
<!-- 最小/最大长度 -->
<input minlength="8" maxlength="128">
<!-- 数值范围 -->
<input type="number" min="1" max="100" step="1">
<!-- 正则匹配 -->
<input type="text" pattern="[A-Za-z0-9]{6,}">
<!-- 邮箱格式 -->
<input type="email">
浏览器在提交前自动校验这些规则,不通过时显示默认提示并阻止提交。novalidate 属性可关闭默认验证(适合用 JavaScript 做自定义校验的场景)。
步骤 4:其他常用输入类型
<!-- 搜索 / 电话 / 日期 -->
<input type="search" name="q" placeholder="搜索课程..." aria-label="搜索">
<input type="tel" name="phone" autocomplete="tel">
<input type="date" name="birthday" min="1900-01-01" max="2026-12-31">
<!-- 单选 -->
<fieldset>
<legend>学习阶段</legend>
<input type="radio" id="beginner" name="level" value="beginner">
<label for="beginner">初学者</label>
<input type="radio" id="advanced" name="level" value="advanced">
<label for="advanced">进阶</label>
</fieldset>
<!-- 下拉选择 -->
<label for="track">学习路线</label>
<select id="track" name="track">
<option value="">请选择</option>
<option value="frontend">前端开发</option>
<option value="backend">后端开发</option>
<option value="fullstack">全栈开发</option>
</select>
fieldset 和 legend 为单选组提供语义分组;autocomplete 帮助浏览器自动填充,提升用户体验。
讲清楚要点
-
<label>与<input>必须显式关联:通过for+id关联,或把<input>嵌套在<label>内。关联后点击标签文本即可聚焦输入框,屏幕阅读器也能正确朗读「邮箱地址,编辑文本」。 -
type决定键盘和验证行为:type="email"在手机上会调出带@和.的键盘;type="tel"调出数字键盘;type="number"限制只能输入数字。选对类型是移动端体验的关键。 -
autocomplete不是可有可无:浏览器通过它识别字段含义,从而提供密码管理器自动填充、地址自动完成等功能。常用值:email、current-password、new-password、name、tel。 -
ARIA 补充 HTML 的不足:
aria-required="true"明确标记必填,aria-describedby="id"把提示文本与输入框关联,aria-invalid="true"在自定义验证时标记错误字段。ARIA 不改变视觉,只改变辅助技术的播报。 -
novalidate的使用场景:当你想用 JavaScript 做统一验证(为了自定义错误样式、统一提示文案、或做异步校验)时,在<form>上加novalidate,阻止浏览器的默认验证气泡。 -
按钮的
type很重要:<button>在<form>内默认type="submit",会触发表单提交。如果只是普通交互按钮,必须写type="button",否则按回车或点击时会意外提交表单。
你可能会懵的地方
-
「
id和name到底有什么区别?」id是页面范围内的唯一标识,用于 CSS 选择器、JavaScript 操作、<label for>关联。name是表单提交时的字段名,同一页面可以有多个相同name(如单选按钮组),但id必须全局唯一。两者通常写成一样,但职责不同。 -
「为什么单选按钮的
name要相同?」 相同name的单选按钮构成互斥组,选中一个会自动取消同组其他按钮的选中状态。name不同则各自独立。 -
「
placeholder能代替<label>吗?」 不能。placeholder在输入内容后消失,用户填到一半忘记这个字段是什么。此外,屏幕阅读器对placeholder的支持不一致。视觉上的标签和辅助技术上的标签都必须存在。 -
「表单一定要包在
<form>标签里吗?」 如果数据要通过传统方式提交到服务器(导致页面刷新),必须有<form>。如果是纯前端应用(如 React/Vue 单页应用),可以用 JS 收集数据并通过fetch发送,此时<form>可选,但保留它有利于可访问性和回退行为。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
<input> 没有对应的 <label> | 点击文字无法聚焦,屏幕阅读器无法识别字段名 | 每个输入字段都要有显式关联的 label |
用 placeholder 代替 label | 输入后标签消失,可访问性差 | label 始终可见,placeholder 仅作示例 |
单选按钮 name 各不相同 | 无法互斥选择 | 同组单选按钮使用相同 name |
按钮不写 type="button" | 在 form 内会意外提交 | 非提交按钮必须声明 type="button" |
忽略 autocomplete | 浏览器无法自动填充,用户体验差 | 根据字段含义提供准确的 autocomplete 值 |
表单无 novalidate 却用 JS 做验证 | 浏览器默认气泡和自定义提示同时出现 | 需要自定义验证时,form 上加 novalidate |
卡点急救
-
点击标签文字,输入框没反应:检查
<label for="x">的x是否与<input id="x">完全一致(区分大小写)。如果使用隐式关联(input 嵌套在 label 内),确保没有写错for。 -
表单提交后页面刷新,看不到结果:检查
<form>的action和method。如果是前端应用,应该用event.preventDefault()阻止默认提交,然后用fetch发送数据。也可以给<form>加novalidate并绑定 JS 提交事件。 -
手机上输入邮箱没有
@符号:检查type是否写成了type="text"。必须是type="email"或type="tel"等特定类型,移动浏览器才会调出对应的优化键盘。
面试常问
-
「如何确保表单的无障碍访问性?」 一、每个
<input>都有关联的<label>(通过for+id或嵌套);二、使用正确的type(如email、tel)以便移动设备调出合适键盘;三、提供autocomplete支持密码管理器和自动填充;四、用aria-describedby关联提示和错误信息;五、确保所有字段可通过键盘Tab访问;六、错误提示不仅用颜色表示,还要配合文字和aria-invalid。 -
「GET 和 POST 提交表单有什么区别?」 GET 把表单数据编码在 URL 查询参数中(如
/search?q=html),适合无副作用的查询操作,有长度限制,会被浏览器历史记录和服务器日志保留。POST 把数据放在请求体中,适合创建/更新操作,无长度限制,相对更安全(但如不配合 HTTPS 仍会被中间人读取)。 -
「HTML5 表单验证和 JavaScript 验证如何配合?」 HTML5 验证(
required、pattern、minlength等)提供即时、轻量的基础校验,不依赖脚本,适合快速反馈。JS 验证处理复杂业务规则(如异步查重、密码强度、多字段联动),并提供统一的错误提示样式。通常做法:form 上加novalidate,JS 拦截 submit 事件,先检查 HTML5 API(input.checkValidity()),再执行业务校验,最后统一渲染错误信息。
自测三问
- 一个登录表单中,邮箱输入框需要哪些属性才能保证可访问性和基础验证?列出至少 5 个。
- 为什么
<label for="email">和<input id="email">必须一一对应?如果不对应,会有哪些问题? - 你想用 JavaScript 做自定义表单验证,同时不想看到浏览器默认的红色提示气泡,应该怎么做?
小结一句话
表单是用户与系统对话的窗口:用 <label> 说清楚问什么,用正确的 type 降低输入成本,用 name 让服务端读懂答案,用 ARIA 确保所有人都能参与这场对话。
延伸
- MDN:HTML 表单基础
- 下一课:HTML 语义化与 ARIA——让代码自己说话。