跳到主要内容

HTML 表单

表单是网页与用户交互的核心机制,用于收集、验证和提交用户数据。从简单的搜索框到复杂的注册流程,表单无处不在。本章将系统介绍 HTML 表单的完整知识体系。

表单的本质

为什么需要表单?

Web 的交互性很大程度上依赖于表单。没有表单,用户只能被动地浏览内容,而无法:

  • 注册账号
  • 搜索信息
  • 提交评论
  • 完成购买
  • 上传文件

表单是用户向服务器发送数据的标准方式,是 Web 应用不可或缺的组成部分。

表单的基本概念

一个完整的表单系统包含三个核心环节:

用户输入 → 客户端验证 → 服务端处理

客户端验证可以提供即时反馈,改善用户体验;服务端验证则是安全性的最后保障。永远不要只依赖客户端验证,因为用户可以绕过前端限制直接提交数据。

表单结构

form 元素

<form> 元素是所有表单控件的容器:

<form action="/submit" method="POST">
<!-- 表单控件 -->
</form>

核心属性:

属性说明示例
action提交目标 URLaction="/api/login"
methodHTTP 方法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位,包含大小写字母和数字">
关于 pattern
  • 正则表达式不需要加 / 包围
  • 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 表单的完整知识:

  1. 表单基础<form> 元素的属性和方法选择
  2. input 类型:文本、数值、日期、选择、文件等各类输入
  3. 其他元素<textarea><select><button><datalist>
  4. 表单组织<label><fieldset><legend> 的语义化使用
  5. 表单验证:内置验证属性、Constraint Validation API、自定义验证
  6. 现代表单特性inputmodeautocomplete、Popover 集成
  7. 无障碍访问:正确的标签关联、错误提示、键盘导航
  8. 最佳实践:安全考虑、性能优化、移动端适配

练习

  1. 创建一个登录表单,包含用户名、密码和记住我选项
  2. 实现一个带实时验证的注册表单
  3. 创建一个包含多种输入类型的调查问卷
  4. 实现密码强度指示器
  5. 创建一个文件上传表单,带预览和大小限制
  6. 实现一个带自定义错误提示的多步骤表单