HTML 表单
表单是网页与用户交互的核心机制,用于收集、验证和提交用户数据。从简单的搜索框到复杂的注册流程,表单无处不在。本章将系统介绍 HTML 表单的完整知识体系。
表单的本质
为什么需要表单?
Web 的交互性很大程度上依赖于表单。没有表单,用户只能被动地浏览内容,而无法:
- 注册账号
- 搜索信息
- 提交评论
- 完成购买
- 上传文件
表单是用户向服务器发送数据的标准方式,是 Web 应用不可或缺的组成部分。
表单的基本概念
一个完整的表单系统包含三个核心环节:
用户输入 → 客户端验证 → 服务端处理
客户端验证可以提供即时反馈,改善用户体验;服务端验证则是安全性的最后保障。永远不要只依赖客户端验证,因为用户可以绕过前端限制直接提交数据。
表单结构
form 元素
<form> 元素是所有表单控件的容器:
<form action="/submit" method="POST">
<!-- 表单控件 -->
</form>
核心属性:
| 属性 | 说明 | 示例 |
|---|---|---|
action | 提交目标 URL | action="/api/login" |
method | HTTP 方法 | method="POST" |
enctype | 编码类型 | enctype="multipart/form-data" |
autocomplete | 自动完成 | autocomplete="on" |
novalidate | 禁用验证 | novalidate |
target | 提交目标窗口 | target="_blank" |
method 的选择
GET 和 POST 是最常用的方法,它们有本质区别:
GET 方法:
- 数据附加在 URL 后面,可见且可收藏
- 有长度限制(浏览器和服务器限制)
- 适合幂等操作(查询、搜索)
- 不应包含敏感信息
<form action="/search" method="GET">
<input type="search" name="q">
<button>搜索</button>
</form>
<!-- 提交后 URL: /search?q=关键词 -->
POST 方法:
- 数据在请求体中,不可见
- 无长度限制
- 适合创建、修改数据
- 可包含敏感信息
<form action="/api/users" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<button>注册</button>
</form>
enctype 编码类型
当表单包含文件上传时,必须设置正确的编码类型:
<!-- 默认:URL 编码 -->
<form enctype="application/x-www-form-urlencoded">
<!-- 适合普通文本字段 -->
</form>
<!-- 文件上传必须使用 -->
<form enctype="multipart/form-data">
<input type="file" name="avatar">
</form>
<!-- 纯文本(极少使用) -->
<form enctype="text/plain">
<!-- 数据以纯文本形式发送 -->
</form>
input 元素详解
<input> 是最灵活的表单元素,通过 type 属性实现不同的输入控件。
文本类输入
text - 单行文本
最基本的输入类型,用于短文本:
<input type="text" name="username"
placeholder="请输入用户名"
maxlength="20"
minlength="3"
pattern="[a-zA-Z][a-zA-Z0-9_]*"
autocomplete="username"
required>
常用属性:
placeholder:占位提示文字maxlength/minlength:字符长度限制pattern:正则表达式验证模式size:显示宽度(字符数)autocomplete:自动完成提示
password - 密码输入
隐藏输入内容,用于敏感信息:
<input type="password" name="password"
minlength="8"
autocomplete="current-password"
required>
密码字段会在传输时明文发送,必须使用 HTTPS 加密。autocomplete="current-password" 帮助密码管理器正确识别字段用途。
email - 邮箱地址
内置邮箱格式验证:
<input type="email" name="email"
placeholder="[email protected]"
autocomplete="email"
multiple> <!-- 允许多个邮箱,逗号分隔 -->
移动端会自动显示带有 @ 符号的键盘布局。
url - 网址
验证 URL 格式:
<input type="url" name="website"
placeholder="https://example.com"
autocomplete="url">
tel - 电话号码
<input type="tel" name="phone"
pattern="[0-9]{11}"
placeholder="13800000000"
autocomplete="tel">
注意:tel 类型不进行格式验证,因为各国电话格式差异很大。需要配合 pattern 属性进行验证。
search - 搜索框
语义化的搜索输入:
<input type="search" name="q"
placeholder="搜索..."
autosave="search-history"
results="5">
部分浏览器会显示搜索历史和清除按钮。
数值类输入
number - 数字输入
<input type="number" name="age"
min="0" max="150" step="1"
placeholder="年龄">
<input type="number" name="price"
min="0" step="0.01"
placeholder="价格">
属性说明:
min/max:数值范围step:步进值(小数精度)
range - 滑块选择
适合选择一个范围内的值:
<input type="range" name="volume"
min="0" max="100" step="1" value="50">
<output for="volume" id="volume-output">50</output>
<script>
const range = document.querySelector('input[name="volume"]');
const output = document.getElementById('volume-output');
range.addEventListener('input', () => {
output.textContent = range.value;
});
</script>
日期时间类输入
HTML5 提供了丰富的日期时间输入类型:
<!-- 日期 -->
<input type="date" name="birthday"
min="1900-01-01" max="2024-12-31">
<!-- 时间 -->
<input type="time" name="alarm" step="60">
<!-- 日期时间(无时区) -->
<input type="datetime-local" name="meeting">
<!-- 月份 -->
<input type="month" name="expiry">
<!-- 周 -->
<input type="week" name="week">
这些类型会在支持的浏览器中显示原生的日期选择器。
选择类输入
checkbox - 复选框
允许选择多个选项:
<fieldset>
<legend>兴趣爱好</legend>
<label>
<input type="checkbox" name="hobby" value="reading"> 阅读
</label>
<label>
<input type="checkbox" name="hobby" value="music"> 音乐
</label>
<label>
<input type="checkbox" name="hobby" value="sports"> 运动
</label>
</fieldset>
重要属性:
checked:默认选中value:选中时提交的值(默认为 "on")
switch 样式(现代浏览器):
<label>
<input type="checkbox" name="darkmode" switch>
深色模式
</label>
switch 属性让复选框呈现开关样式(Chrome 120+ 支持)。
radio - 单选按钮
同一组中只能选择一个:
<fieldset>
<legend>性别</legend>
<label>
<input type="radio" name="gender" value="male" checked> 男
</label>
<label>
<input type="radio" name="gender" value="female"> 女
</label>
<label>
<input type="radio" name="gender" value="other"> 其他
</label>
</fieldset>
关键点:同组单选按钮必须有相同的 name 属性值。
文件上传
<!-- 单文件上传 -->
<input type="file" name="document">
<!-- 多文件上传 -->
<input type="file" name="photos" multiple>
<!-- 限制文件类型 -->
<input type="file" name="avatar" accept="image/*">
<input type="file" name="doc" accept=".pdf,.doc,.docx">
<input type="file" name="image" accept="image/png,image/jpeg">
<!-- 捕获设备 -->
<input type="file" accept="image/*" capture="environment"> <!-- 后置摄像头 -->
<input type="file" accept="image/*" capture="user"> <!-- 前置摄像头 -->
accept 属性值:
- 文件扩展名:
.pdf,.doc - MIME 类型:
image/*,application/pdf - 组合:
image/png,image/jpeg
其他类型
<!-- 颜色选择 -->
<input type="color" name="theme" value="#3498db">
<!-- 隐藏字段 -->
<input type="hidden" name="user_id" value="12345">
<input type="hidden" name="token" value="abc123">
<!-- 图像提交按钮 -->
<input type="image" src="submit.png" alt="提交">
<!-- 按钮 -->
<input type="button" value="按钮">
<input type="submit" value="提交">
<input type="reset" value="重置">
其他表单元素
textarea - 多行文本
<textarea name="message"
rows="5"
cols="40"
minlength="10"
maxlength="500"
placeholder="请输入留言..."
wrap="hard"></textarea>
属性说明:
rows:可见行数cols:可见列数wrap:换行方式(soft不保留换行,hard保留换行)
select - 下拉选择
<!-- 基本下拉 -->
<select name="country">
<option value="">请选择国家</option>
<option value="cn">中国</option>
<option value="us">美国</option>
<option value="jp">日本</option>
</select>
<!-- 分组选项 -->
<select name="city">
<optgroup label="北京市">
<option value="bj-dc">东城区</option>
<option value="bj-xc">西城区</option>
</optgroup>
<optgroup label="上海市">
<option value="sh-hp">黄浦区</option>
<option value="sh-xh">徐汇区</option>
</optgroup>
</select>
<!-- 多选 -->
<select name="skills" multiple size="4">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="js">JavaScript</option>
<option value="python">Python</option>
</select>
button - 按钮
<!-- 提交按钮 -->
<button type="submit">提交</button>
<!-- 重置按钮 -->
<button type="reset">重置</button>
<!-- 普通按钮 -->
<button type="button">点击</button>
<!-- 带图标的按钮 -->
<button type="submit">
<svg><!-- 图标 --></svg>
提交
</button>
<!-- 禁用表单验证 -->
<button type="submit" formnovalidate>保存草稿</button>
datalist - 预设选项
提供输入建议,同时允许自定义输入:
<input list="browsers" name="browser" placeholder="选择浏览器">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>
meter 和 progress - 度量显示
<!-- 已知范围的度量 -->
<meter value="75" min="0" max="100">75%</meter>
<meter value="0.6">60%</meter>
<!-- 进度条 -->
<progress value="70" max="100">70%</progress>
<progress>加载中...</progress> <!-- 不确定进度 -->
表单组织与语义
label 标签关联
<label> 为表单控件提供说明文字,有两种关联方式:
<!-- 方式一:包裹关联 -->
<label>
用户名:<input type="text" name="username">
</label>
<!-- 方式二:for 属性关联 -->
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
使用 label 的好处:
- 点击标签文字自动聚焦到输入框
- 屏幕阅读器可以朗读标签内容
- 增大点击区域,改善用户体验
每个表单控件都应该有关联的 <label>,这是无障碍访问的基本要求。
fieldset 分组
<fieldset> 用于对相关表单控件分组:
<form>
<fieldset>
<legend>个人信息</legend>
<label for="name">姓名:</label>
<input type="text" id="name" name="name">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</fieldset>
<fieldset>
<legend>教育背景</legend>
<label for="school">学校:</label>
<input type="text" id="school" name="school">
</fieldset>
<button type="submit">提交</button>
</form>
<legend> 是 <fieldset> 的标题,对于可访问性非常重要。
form 属性关联
表单控件可以放在 <form> 外部,通过 form 属性关联:
<form id="myForm" action="/submit">
<input type="text" name="username">
<button type="submit">提交</button>
</form>
<!-- 这个输入框在 form 外部,但仍属于该表单 -->
<input type="text" name="extra" form="myForm" placeholder="额外的字段">
表单验证
表单验证是确保用户输入符合预期的关键环节。HTML5 提供了强大的内置验证机制。
内置验证属性
required - 必填验证
<input type="text" name="username" required>
<input type="email" name="email" required>
<select name="country" required>
<option value="">请选择</option>
<option value="cn">中国</option>
</select>
type 验证
某些 type 值会自动进行格式验证:
<!-- 自动验证邮箱格式 -->
<input type="email" name="email">
<!-- 自动验证 URL 格式 -->
<input type="url" name="website">
<!-- 自动验证数字范围 -->
<input type="number" name="age" min="0" max="150">
pattern - 正则验证
使用正则表达式进行自定义验证:
<!-- 手机号验证 -->
<input type="tel" name="phone"
pattern="1[3-9]\d{9}"
title="请输入11位手机号">
<!-- 用户名验证(字母开头,4-16位) -->
<input type="text" name="username"
pattern="[a-zA-Z][a-zA-Z0-9_]{3,15}"
title="用户名必须以字母开头,4-16位字母数字或下划线">
<!-- 邮编验证 -->
<input type="text" name="zipcode"
pattern="[1-9]\d{5}"
title="请输入6位邮政编码">
<!-- 密码强度 -->
<input type="password" name="password"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="密码至少8位,包含大小写字母和数字">
- 正则表达式不需要加
/包围 title属性提供验证失败的提示信息- 验证是对整个输入值进行匹配
长度验证
<!-- 最小/最大长度 -->
<input type="text" name="username" minlength="3" maxlength="20">
<textarea name="content" minlength="10" maxlength="500"></textarea>
数值范围验证
<!-- 年龄范围 -->
<input type="number" name="age" min="0" max="150">
<!-- 日期范围 -->
<input type="date" name="birthday" min="1900-01-01" max="2024-12-31">
<!-- 步进值 -->
<input type="number" name="price" min="0" step="0.01">
Constraint Validation API
HTML5 的约束验证 API 提供了编程方式来控制和检查验证状态。
基本方法
const input = document.querySelector('input[name="email"]');
// 检查有效性(返回布尔值)
input.checkValidity(); // true / false
// 检查并报告(显示浏览器默认提示)
input.reportValidity();
// 设置自定义验证消息
input.setCustomValidity('请输入有效的邮箱地址');
// 清除自定义消息
input.setCustomValidity('');
ValidityState 对象
每个表单元素都有 validity 属性,包含详细的验证状态:
const input = document.querySelector('input');
input.validity.valid // 是否全部验证通过
input.validity.valueMissing // required 但为空
input.validity.typeMismatch // 类型不匹配(email、url)
input.validity.patternMismatch // 不匹配 pattern
input.validity.tooLong // 超过 maxlength
input.validity.tooShort // 低于 minlength
input.validity.rangeUnderflow // 小于 min
input.validity.rangeOverflow // 大于 max
input.validity.stepMismatch // 不符合 step
input.validity.badInput // 浏览器无法转换的输入
input.validity.customError // 有自定义错误
自定义验证示例
<form id="registerForm">
<label>
密码:
<input type="password" id="password" name="password" required>
</label>
<label>
确认密码:
<input type="password" id="confirmPassword" name="confirmPassword" required>
</label>
<button type="submit">注册</button>
</form>
<script>
const form = document.getElementById('registerForm');
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
function validatePassword() {
if (password.value !== confirmPassword.value) {
confirmPassword.setCustomValidity('两次密码输入不一致');
} else {
confirmPassword.setCustomValidity('');
}
}
password.addEventListener('input', validatePassword);
confirmPassword.addEventListener('input', validatePassword);
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
// 显示第一个错误
const firstInvalid = form.querySelector(':invalid');
firstInvalid.focus();
}
});
</script>
文件大小验证
<form id="uploadForm">
<input type="file" id="fileInput" name="file" accept="image/*">
<button type="submit">上传</button>
</form>
<script>
const fileInput = document.getElementById('fileInput');
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (file && file.size > MAX_SIZE) {
this.setCustomValidity('文件大小不能超过 5MB');
} else {
this.setCustomValidity('');
}
});
</script>
CSS 验证样式
CSS 伪类可以根据验证状态设置样式:
/* 必填字段 */
input:required {
border-left: 3px solid #3498db;
}
/* 可选字段 */
input:optional {
border-left: 3px solid transparent;
}
/* 验证通过 */
input:valid {
border-color: #2ecc71;
}
/* 验证失败 */
input:invalid {
border-color: #e74c3c;
}
/* 用户交互后的验证状态 */
input:user-valid {
border-color: #2ecc71;
}
input:user-invalid {
border-color: #e74c3c;
}
/* 组合使用:有内容时才显示验证状态 */
input:not(:placeholder-shown):valid {
border-color: #2ecc71;
background-image: url('check.svg');
background-position: right 10px center;
background-repeat: no-repeat;
}
input:not(:placeholder-shown):invalid {
border-color: #e74c3c;
background-image: url('error.svg');
background-position: right 10px center;
background-repeat: no-repeat;
}
/* 焦点状态 */
input:focus:invalid {
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2);
}
input:focus:valid {
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
}
/* 范围外值 */
input:out-of-range {
border-color: orange;
}
/* 范围内值 */
input:in-range {
border-color: green;
}
/* 占位符显示时 */
input:placeholder-shown {
border-style: dashed;
}
验证事件
const form = document.querySelector('form');
const input = document.querySelector('input');
// 表单提交前验证
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
// 自定义错误处理
}
});
// 输入值变化
input.addEventListener('input', () => {
// 实时验证
});
// 值提交(Enter 键或失去焦点)
input.addEventListener('change', () => {
// 验证逻辑
});
// 无效事件(验证失败时触发)
input.addEventListener('invalid', (e) => {
e.preventDefault(); // 阻止默认提示
// 自定义错误提示
});
禁用验证
<!-- 整个表单禁用验证 -->
<form novalidate>
...
</form>
<!-- 特定提交按钮禁用验证 -->
<button type="submit">提交</button>
<button type="submit" formnovalidate>保存草稿</button>
现代表单特性
inputmode 属性
inputmode 提示移动设备显示合适的虚拟键盘:
<!-- 数字键盘 -->
<input type="text" inputmode="numeric" placeholder="验证码">
<!-- 电话键盘 -->
<input type="text" inputmode="tel" placeholder="电话号码">
<!-- 邮箱键盘(带 @) -->
<input type="text" inputmode="email" placeholder="邮箱">
<!-- URL 键盘(带 / 和 .com) -->
<input type="text" inputmode="url" placeholder="网址">
<!-- 小数键盘 -->
<input type="text" inputmode="decimal" placeholder="金额">
<!-- 搜索键盘 -->
<input type="text" inputmode="search" placeholder="搜索">
inputmode 与 type 的区别:
type决定数据类型和验证规则inputmode只影响虚拟键盘布局- 当需要自定义验证逻辑时,可使用
type="text"配合inputmode
autocomplete 属性
帮助浏览器和密码管理器正确填写表单:
<!-- 用户名 -->
<input type="text" name="username" autocomplete="username">
<!-- 密码 -->
<input type="password" autocomplete="current-password"> <!-- 登录 -->
<input type="password" autocomplete="new-password"> <!-- 注册 -->
<!-- 个人信息 -->
<input type="text" name="name" autocomplete="name">
<input type="email" autocomplete="email">
<input type="tel" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
<!-- 支付信息 -->
<input type="text" autocomplete="cc-number"> <!-- 卡号 -->
<input type="text" autocomplete="cc-exp"> <!-- 有效期 -->
<input type="text" autocomplete="cc-csc"> <!-- 安全码 -->
<!-- 一次性密码 -->
<input type="text" autocomplete="one-time-code">
常用 autocomplete 值:
| 值 | 用途 |
|---|---|
on | 启用自动完成 |
off | 禁用自动完成 |
name | 全名 |
given-name | 名 |
family-name | 姓 |
email | 邮箱地址 |
tel | 电话号码 |
username | 用户名 |
current-password | 当前密码 |
new-password | 新密码 |
one-time-code | 一次性验证码 |
Popover 集成
现代浏览器支持 Popover API 与表单的集成:
<button type="button" popovertarget="help-panel">
帮助
</button>
<div popover id="help-panel">
<h3>填写说明</h3>
<p>用户名需要以字母开头...</p>
<button type="button" popovertarget="help-panel" popovertargetaction="hide">
关闭
</button>
</div>
dirname 属性
提交文本输入的方向:
<form action="/submit">
<input type="text" name="comment" dirname="comment-dir">
<button>提交</button>
</form>
<!-- 如果用户输入中文或英文,提交数据为:
comment=Hello&comment-dir=ltr
如果用户输入阿拉伯语,则为:
comment=مرحبا&comment-dir=rtl
-->
无障碍访问
正确使用 label
<!-- 好:显式关联 -->
<label for="email">邮箱地址:</label>
<input type="email" id="email" name="email">
<!-- 好:隐式关联 -->
<label>
邮箱地址:
<input type="email" name="email">
</label>
<!-- 不好:没有 label -->
<input type="email" placeholder="邮箱地址">
必填字段标识
<!-- 使用视觉标记 -->
<label for="email">
邮箱地址 <span aria-hidden="true">*</span>
<span class="visually-hidden">(必填)</span>
</label>
<input type="email" id="email" name="email" required>
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
错误提示
<label for="email">邮箱地址:</label>
<input type="email" id="email" name="email"
aria-describedby="email-error"
aria-invalid="true">
<span id="email-error" role="alert">
请输入有效的邮箱地址
</span>
关键属性:
aria-describedby:关联错误提示元素aria-invalid:标记输入无效role="alert":让屏幕阅读器立即朗读
分组关联
<fieldset aria-describedby="personal-info-desc">
<legend>个人信息</legend>
<p id="personal-info-desc" class="visually-hidden">
请填写您的基本个人信息
</p>
<label for="name">姓名:</label>
<input type="text" id="name" name="name">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</fieldset>
键盘导航
确保表单可以通过键盘完全操作:
/* 显示焦点样式 */
input:focus-visible,
button:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid #3498db;
outline-offset: 2px;
}
/* 跳过不需要导航的元素 */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
最佳实践
1. 表单设计原则
- 简洁性:只收集必要的信息
- 清晰性:标签和提示要明确
- 即时反馈:实时验证,及时提示错误
- 容错性:允许用户修改错误
2. 安全考虑
<!-- 密码字段使用正确的 autocomplete -->
<input type="password" autocomplete="new-password">
<!-- 敏感表单使用 POST 方法 -->
<form method="POST">
<!-- 文件上传限制类型和大小 -->
<input type="file" accept=".pdf" />
服务端验证不可省略:
- 客户端验证可被绕过
- 用户可通过开发者工具修改 HTML
- 可直接构造 HTTP 请求
3. 性能优化
<!-- 图片文件预览 -->
<input type="file" id="avatar" accept="image/*">
<img id="preview" src="" alt="预览" style="display: none;">
<script>
document.getElementById('avatar').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById('preview');
preview.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
</script>
4. 移动端优化
<!-- 使用合适的输入类型触发正确的键盘 -->
<input type="tel" inputmode="numeric" pattern="[0-9]*">
<!-- 设置合适的字体大小(避免 iOS 自动缩放) -->
input, select, textarea {
font-size: 16px;
}
<!-- 禁用自动大写 -->
<input type="text" autocapitalize="off">
<!-- 禁用自动纠正 -->
<input type="text" autocorrect="off">
完整示例:注册表单
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
background-color: #f5f5f5;
padding: 20px;
}
.form-container {
max-width: 500px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.required::after {
content: ' *';
color: #e74c3c;
}
input, select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
input:not(:placeholder-shown):valid {
border-color: #2ecc71;
}
input:not(:placeholder-shown):invalid {
border-color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 14px;
margin-top: 4px;
display: none;
}
input:user-invalid + .error-message {
display: block;
}
.password-strength {
height: 4px;
background: #ddd;
margin-top: 8px;
border-radius: 2px;
overflow: hidden;
}
.password-strength-bar {
height: 100%;
width: 0;
transition: width 0.3s, background-color 0.3s;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input {
width: auto;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
button {
flex: 1;
padding: 12px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #ecf0f1;
color: #333;
}
.btn-secondary:hover {
background-color: #ddd;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<main class="form-container">
<h1>用户注册</h1>
<form id="registerForm" action="/api/register" method="POST" novalidate>
<div class="form-group">
<label for="username" class="required">用户名</label>
<input type="text"
id="username"
name="username"
required
minlength="3"
maxlength="20"
pattern="[a-zA-Z][a-zA-Z0-9_]*"
placeholder="字母开头,3-20位字母数字下划线"
autocomplete="username"
aria-describedby="username-error">
<p id="username-error" class="error-message">
用户名必须以字母开头,3-20位字母数字或下划线
</p>
</div>
<div class="form-group">
<label for="email" class="required">邮箱</label>
<input type="email"
id="email"
name="email"
required
placeholder="[email protected]"
autocomplete="email"
aria-describedby="email-error">
<p id="email-error" class="error-message">
请输入有效的邮箱地址
</p>
</div>
<div class="form-group">
<label for="password" class="required">密码</label>
<input type="password"
id="password"
name="password"
required
minlength="8"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
placeholder="至少8位,包含大小写字母和数字"
autocomplete="new-password"
aria-describedby="password-error password-hint">
<div class="password-strength">
<div class="password-strength-bar" id="strengthBar"></div>
</div>
<p id="password-hint" class="visually-hidden">
密码至少8位,需包含大小写字母和数字
</p>
<p id="password-error" class="error-message">
密码强度不足
</p>
</div>
<div class="form-group">
<label for="confirmPassword" class="required">确认密码</label>
<input type="password"
id="confirmPassword"
name="confirmPassword"
required
placeholder="再次输入密码"
autocomplete="new-password"
aria-describedby="confirm-error">
<p id="confirm-error" class="error-message">
两次密码输入不一致
</p>
</div>
<div class="form-group">
<label for="phone">手机号</label>
<input type="tel"
id="phone"
name="phone"
pattern="1[3-9]\d{9}"
placeholder="选填"
autocomplete="tel">
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox"
id="agree"
name="agree"
required>
<label for="agree">
我已阅读并同意<a href="/terms">用户协议</a>
</label>
</div>
</div>
<div class="button-group">
<button type="submit" class="btn-primary">注册</button>
<button type="reset" class="btn-secondary">重置</button>
</div>
</form>
</main>
<script>
const form = document.getElementById('registerForm');
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
const strengthBar = document.getElementById('strengthBar');
// 密码强度检测
password.addEventListener('input', function() {
const value = this.value;
let strength = 0;
if (value.length >= 8) strength += 25;
if (/[a-z]/.test(value)) strength += 25;
if (/[A-Z]/.test(value)) strength += 25;
if (/\d/.test(value)) strength += 25;
strengthBar.style.width = strength + '%';
if (strength <= 25) {
strengthBar.style.backgroundColor = '#e74c3c';
} else if (strength <= 50) {
strengthBar.style.backgroundColor = '#f39c12';
} else if (strength <= 75) {
strengthBar.style.backgroundColor = '#f1c40f';
} else {
strengthBar.style.backgroundColor = '#2ecc71';
}
validatePasswordMatch();
});
// 密码匹配验证
function validatePasswordMatch() {
if (confirmPassword.value && password.value !== confirmPassword.value) {
confirmPassword.setCustomValidity('两次密码输入不一致');
} else {
confirmPassword.setCustomValidity('');
}
}
confirmPassword.addEventListener('input', validatePasswordMatch);
// 表单提交
form.addEventListener('submit', function(e) {
if (!form.checkValidity()) {
e.preventDefault();
// 显示第一个错误
const firstInvalid = form.querySelector(':invalid');
if (firstInvalid) {
firstInvalid.focus();
firstInvalid.reportValidity();
}
}
});
</script>
</body>
</html>
小结
本章系统学习了 HTML 表单的完整知识:
- 表单基础:
<form>元素的属性和方法选择 - input 类型:文本、数值、日期、选择、文件等各类输入
- 其他元素:
<textarea>、<select>、<button>、<datalist>等 - 表单组织:
<label>、<fieldset>、<legend>的语义化使用 - 表单验证:内置验证属性、Constraint Validation API、自定义验证
- 现代表单特性:
inputmode、autocomplete、Popover 集成 - 无障碍访问:正确的标签关联、错误提示、键盘导航
- 最佳实践:安全考虑、性能优化、移动端适配
练习
- 创建一个登录表单,包含用户名、密码和记住我选项
- 实现一个带实时验证的注册表单
- 创建一个包含多种输入类型的调查问卷
- 实现密码强度指示器
- 创建一个文件上传表单,带预览和大小限制
- 实现一个带自定义错误提示的多步骤表单