fieldset 与 legend
<fieldset> 和 <legend> 是 HTML 中用于对表单控件进行逻辑分组的元素。<fieldset> 创建一个分组容器,<legend> 为该分组提供标题。它们不仅改善了表单的视觉组织,更重要的是提升了可访问性——屏幕阅读器可以宣读分组标题,帮助用户理解表单结构。
前置知识
阅读本节前,建议先了解:meter 度量元素
基础概念
什么是 fieldset 和 legend
<fieldset>:将相关的表单控件组织成一个逻辑组,浏览器默认渲染一个边框包围<legend>:为<fieldset>提供标题,显示在边框上方
主要用途:
- 视觉分组:用边框和标题将相关控件组织在一起
- 可访问性:屏幕阅读器会朗读 legend,帮助用户理解表单结构
- 批量操作:通过
disabled属性一键禁用整个字段组 - 语义化:表达"这些控件属于同一组"的关系
浏览器支持
<fieldset> 和 <legend> 是 HTML 最古老的元素之一,所有浏览器都完整支持。
语法
基本用法
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fieldset 基本示例</title>
</head>
<body>
<h2>用户注册</h2>
<form action="/register" method="post">
<fieldset>
<legend>个人信息</legend>
<div>
<label for="name">姓名:</label>
<input type="text" id="name" name="name">
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</div>
</fieldset>
<fieldset>
<legend>安全设置</legend>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password">
</div>
<div>
<label for="confirm">确认密码:</label>
<input type="password" id="confirm" name="confirm">
</div>
</fieldset>
<button type="submit">注册</button>
</form>
</body>
</html>详细说明
CSS 自定义样式
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fieldset CSS</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
/* 默认样式去除 */
fieldset {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
/* legend 样式 */
legend {
font-weight: 600;
font-size: 16px;
padding: 0 8px;
color: #333;
}
/* 卡片式风格 */
.card-fieldset {
border: 2px solid #4a90d9;
background: #f8faff;
border-radius: 12px;
}
.card-fieldset legend {
color: #4a90d9;
font-size: 18px;
}
/* 无边框风格 */
.borderless-fieldset {
border: none;
padding: 0;
margin-bottom: 24px;
}
.borderless-fieldset legend {
font-size: 18px;
border-bottom: 2px solid #eee;
padding-bottom: 8px;
width: 100%;
}
/* 禁用状态 */
fieldset:disabled {
opacity: 0.5;
background: #f5f5f5;
}
.form-row {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 4px;
}
input {
padding: 6px 10px;
border: 2px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<h2>fieldset 样式变体</h2>
<form>
<!-- 默认样式 -->
<fieldset>
<legend>联系方式</legend>
<div class="form-row">
<label for="phone">手机:</label>
<input type="tel" id="phone" name="phone">
</div>
<div class="form-row">
<label for="addr">地址:</label>
<input type="text" id="addr" name="address">
</div>
</fieldset>
<!-- 卡片样式 -->
<fieldset class="card-fieldset">
<legend>账户信息</legend>
<div class="form-row">
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div class="form-row">
<label for="pwd">密码:</label>
<input type="password" id="pwd" name="password">
</div>
</fieldset>
<!-- 无边框样式 -->
<fieldset class="borderless-fieldset">
<legend>偏好设置</legend>
<div class="form-row">
<label>
<input type="checkbox" name="newsletter" checked>
订阅新闻邮件
</label>
</div>
<div class="form-row">
<label>
<input type="checkbox" name="sms" checked>
接收短信通知
</label>
</div>
</fieldset>
</form>
</body>
</html>disabled 禁用整组
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fieldset disabled</title>
</head>
<body>
<h2>地址信息</h2>
<form>
<fieldset>
<legend>是否需要填写地址</legend>
<label>
<input type="checkbox" id="need-address" name="need_address" onchange="toggleAddress(this.checked)">
我需要填写收货地址
</label>
</fieldset>
<!-- 整组禁用:checked 后才启用 -->
<fieldset id="address-fields" disabled>
<legend>收货地址</legend>
<div>
<label for="province">省份:</label>
<select id="province" name="province">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangdong">广东</option>
</select>
</div>
<div>
<label for="detail">详细地址:</label>
<input type="text" id="detail" name="detail" disabled>
</div>
</fieldset>
<button type="submit">提交</button>
</form>
<script>
function toggleAddress(checked) {
document.getElementById('address-fields').disabled = !checked;
// fieldset disabled 会自动禁用内部所有控件
}
</script>
</body>
</html>可访问性优势
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fieldset 可访问性</title>
</head>
<body>
<h2>fieldset 的可访问性</h2>
<form>
<!-- 屏幕阅读器会朗读:"个人信息 分组" -->
<fieldset>
<legend>个人信息</legend>
<p><label for="first-name">名:<input type="text" id="first-name"></label></p>
<p><label for="last-name">姓:<input type="text" id="last-name"></label></p>
</fieldset>
<!-- 屏幕阅读器会朗读:"联系方式 分组" -->
<fieldset>
<legend>联系方式</legend>
<p><label for="tel">电话:<input type="tel" id="tel"></label></p>
<p><label for="email">邮箱:<input type="email" id="email"></label></p>
</fieldset>
</form>
<h3>ARIA 等价方式</h3>
<p>
如果无法使用 fieldset/legend,可以通过 ARIA role 实现类似效果:
</p>
<div role="group" aria-labelledby="group-title">
<h3 id="group-title">付款信息</h3>
<p><label for="card">卡号:<input type="text" id="card"></label></p>
</div>
</body>
</html>嵌套 fieldset
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>嵌套 fieldset</title>
</head>
<body>
<h2>嵌套分组</h2>
<form>
<fieldset>
<legend>收货信息</legend>
<p><label for="name">收件人:<input type="text" id="name"></label></p>
<!-- 嵌套分组 -->
<fieldset>
<legend>地址详情</legend>
<p><label for="city">城市:<input type="text" id="city"></label></p>
<p><label for="street">街道:<input type="text" id="street"></label></p>
</fieldset>
<p><label for="phone">电话:<input type="tel" id="phone"></label></p>
</fieldset>
</form>
</body>
</html>实战示例
多步注册表单
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>多步注册表单</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
fieldset {
border: 2px solid #4a90d9;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
transition: opacity 0.3s;
}
fieldset:disabled {
opacity: 0.4;
border-color: #ccc;
}
legend {
font-size: 18px;
color: #4a90d9;
font-weight: 600;
padding: 0 12px;
}
.form-row { margin-bottom: 10px; }
label { display: block; margin-bottom: 4px; font-weight: 500; }
input {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
}
input:focus { border-color: #4a90d9; outline: none; }
.actions { display: flex; gap: 8px; margin-top: 16px; }
button {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.btn-primary { background: #4a90d9; color: #fff; }
.btn-primary:disabled { background: #bbb; cursor: not-allowed; }
.btn-secondary { background: #eee; }
.btn-secondary:disabled { display: none; }
.step-indicator {
display: flex;
gap: 4px;
margin-bottom: 16px;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ddd;
transition: background 0.3s;
}
.step-dot.active { background: #4a90d9; }
</style>
</head>
<body>
<h2>注册账号</h2>
<form>
<div class="step-indicator">
<div class="step-dot active" id="dot-1"></div>
<div class="step-dot" id="dot-2"></div>
<div class="step-dot" id="dot-3"></div>
</div>
<fieldset id="step-1">
<legend>第一步:基本信息</legend>
<div class="form-row">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-row">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
</div>
</fieldset>
<fieldset id="step-2" disabled>
<legend>第二步:设置密码</legend>
<div class="form-row">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-row">
<label for="confirm-pwd">确认密码:</label>
<input type="password" id="confirm-pwd" name="confirm_pwd" required>
</div>
</fieldset>
<fieldset id="step-3" disabled>
<legend>第三步:个人资料</legend>
<div class="form-row">
<label for="nickname">昵称:</label>
<input type="text" id="nickname" name="nickname">
</div>
<div class="form-row">
<label for="birthday">生日:</label>
<input type="date" id="birthday" name="birthday">
</div>
</fieldset>
<div class="actions">
<button type="button" id="prev-btn" class="btn-secondary" disabled>上一步</button>
<button type="button" id="next-btn" class="btn-primary">下一步</button>
<button type="submit" id="submit-btn" class="btn-primary" style="display: none;">完成注册</button>
</div>
</form>
<script>
let currentStep = 1;
const totalSteps = 3;
function showStep(step) {
for (let i = 1; i <= totalSteps; i++) {
const fs = document.getElementById(`step-${i}`);
fs.disabled = i !== step;
document.getElementById(`dot-${i}`).classList.toggle('active', i === step);
}
document.getElementById('prev-btn').disabled = step === 1;
document.getElementById('next-btn').style.display = step === totalSteps ? 'none' : '';
document.getElementById('submit-btn').style.display = step === totalSteps ? '' : 'none';
currentStep = step;
}
document.getElementById('next-btn').addEventListener('click', () => {
if (currentStep < totalSteps) showStep(currentStep + 1);
});
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentStep > 1) showStep(currentStep - 1);
});
</script>
</body>
</html>注意事项
fieldset 和 div 的区别
使用 <div> 配合 CSS 边框也能实现类似视觉效果,但缺少语义化和可访问性支持:
html
<!-- 不推荐:无语义化 -->
<div style="border: 1px solid #ddd; padding: 12px;">
<h3>个人信息</h3>
<input type="text" name="name">
</div>
<!-- 推荐:语义化分组 -->
<fieldset>
<legend>个人信息</legend>
<input type="text" name="name">
</fieldset>legend 的位置限制
<legend> 必须是 <fieldset> 的第一个子元素,否则不会正确显示。
disabled 会影响所有子控件
对 <fieldset> 设置 disabled 后,内部的所有表单控件(input、select、textarea、button)都会被禁用,值也不会提交。
最佳实践
- 按功能分组:将逻辑相关的表单控件放在同一个 fieldset 中
- 使用有意义的 legend:legend 应简洁明了地描述该组的功能
- 利用 disabled 控制条件字段:减少大量 JavaScript 操作
- CSS 统一样式:通过 CSS 让 fieldset/legend 与页面风格一致
- 多层嵌套要谨慎:避免超过 2 层嵌套,保持结构清晰
下一节
继续学习:name 与 value