optgroup 选项分组
<optgroup> 是 <select> 元素内部的分组容器,用于将相关的 <option> 选项组织成逻辑分组,每组有一个不可选择的标题。通过分组,用户可以更快地在大量选项中定位目标。本节将详细讲解 <optgroup> 的语法、label 分组标题、disabled 禁用分组,以及分组在无障碍访问中的作用。
前置知识
阅读本节前,建议先了解:datalist 数据列表
基础概念
什么是 optgroup
<optgroup> 是 <select> 的子元素,用于将 <option> 选项按类别分组。每个 <optgroup> 通过 label 属性设置组标题,该标题以不可选择的粗体文本形式显示在下拉列表中。
<select name="category">
<optgroup label="前端技术">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="javascript">JavaScript</option>
</optgroup>
<optgroup label="后端技术">
<option value="nodejs">Node.js</option>
<option value="python">Python</option>
<option value="java">Java</option>
</optgroup>
</select>optgroup 的核心特征
| 特征 | 说明 |
|---|---|
| 位置 | 必须在 <select> 内部 |
| 子元素 | 只能包含 <option> |
| 标题 | 通过 label 属性设置,不可选择 |
| 嵌套 | 不支持嵌套(不支持多级分组) |
| 禁用 | 可设置 disabled 禁用整组选项 |
| 提交 | 不提交任何数据(不是表单控件) |
语法
基本语法
<select name="fieldName">
<!-- 无分组的选项 -->
<option value="">请选择</option>
<!-- 分组一 -->
<optgroup label="分组标题1">
<option value="v1">选项一</option>
<option value="v2">选项二</option>
</optgroup>
<!-- 分组二 -->
<optgroup label="分组标题2" disabled>
<option value="v3">选项三</option>
<option value="v4">选项四</option>
</optgroup>
</select>属性说明
| 属性 | 值 | 说明 |
|---|---|---|
label | 字符串 | 必需,分组标题文本 |
disabled | 布尔属性 | 禁用整个分组 |
详细说明
label 分组标题
label 属性是 <optgroup> 的必需属性,设置分组的标题。标题以下拉列表中的粗体/斜体文本显示(不同浏览器样式略有差异),用户无法点击选择。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>optgroup label</title>
<style>
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-weight: 500;
margin-bottom: 6px;
}
select {
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
min-width: 250px;
}
</style>
</head>
<body>
<h2>选择技术方向</h2>
<form action="/course" method="post">
<div class="form-group">
<label for="tech">技术分类:</label>
<select id="tech" name="tech" required>
<option value="">请选择技术方向</option>
<!--
label 是分组标题
在下拉列表中显示为不可选的粗体文本
-->
<optgroup label="前端开发">
<option value="html-css">HTML / CSS</option>
<option value="javascript">JavaScript 基础</option>
<option value="vue">Vue.js</option>
<option value="react">React</option>
<option value="angular">Angular</option>
</optgroup>
<optgroup label="后端开发">
<option value="nodejs">Node.js</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
</optgroup>
<optgroup label="移动开发">
<option value="ios-swift">iOS (Swift)</option>
<option value="android-kotlin">Android (Kotlin)</option>
<option value="flutter">Flutter</option>
<option value="react-native">React Native</option>
</optgroup>
<optgroup label="DevOps">
<option value="docker">Docker</option>
<option value="kubernetes">Kubernetes</option>
<option value="ci-cd">CI/CD</option>
</optgroup>
</select>
</div>
<button type="submit">开始学习</button>
</form>
</body>
</html>disabled 禁用分组
<optgroup> 的 disabled 属性会禁用整个分组中的所有选项。被禁用的选项在下拉列表中显示为灰色,用户无法选择。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>optgroup disabled</title>
<style>
select {
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
min-width: 300px;
}
</style>
</head>
<body>
<h2>选择配送方式</h2>
<form action="/order" method="post">
<label for="shipping">配送方式:</label>
<select id="shipping" name="shipping" required>
<optgroup label="标准配送">
<option value="standard" selected>标准快递(3~5天,免费)</option>
<option value="express">加急快递(1~2天,¥15)</option>
</optgroup>
<!--
禁用整个分组
可能是暂时不可用的服务
-->
<optgroup label="特殊配送" disabled>
<option value="sameday">当日达(¥30)</option>
<option value="scheduled">定时达(¥20)</option>
<option value="pickup">门店自提(免费)</option>
</optgroup>
<optgroup label="国际配送">
<option value="intl-standard">国际标准(7~15天,¥50起)</option>
<option value="intl-express">国际加急(3~5天,¥100起)</option>
</optgroup>
</select>
<button type="submit">提交订单</button>
</form>
<!--
用户看到的效果:
- "特殊配送" 分组标题和所有选项都显示为灰色
- 无法选择 "当日达"、"定时达"、"门店自提"
- 其他分组的选项正常可选
-->
</body>
</html>optgroup disabled 与单个 option disabled
optgroup disabled禁用整个组- 单个
<option disabled>禁用单个选项 - 如果一个
<option>既在 disabled 的 optgroup 中,自身也设置了 disabled,效果相同 - 可以通过 JavaScript 动态修改
disabled属性来启用/禁用分组
混合使用:有分组和无分组选项
<select> 中可以同时存在无分组的 <option> 和有分组的 <optgroup>:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>混合分组</title>
<style>
select {
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
min-width: 300px;
}
</style>
</head>
<body>
<h2>字体选择</h2>
<form>
<label for="font">选择字体:</label>
<select id="font" name="font">
<!-- 无分组的默认选项 -->
<option value="">使用默认字体</option>
<option value="system-ui">系统默认 (system-ui)</option>
<!-- 有分组的选项 -->
<optgroup label="无衬线字体">
<option value="arial">Arial</option>
<option value="helvetica">Helvetica</option>
<option value="verdana">Verdana</option>
<option value="tahoma">Tahoma</option>
</optgroup>
<optgroup label="衬线字体">
<option value="times">Times New Roman</option>
<option value="georgia">Georgia</option>
<option value="palatino">Palatino</option>
</optgroup>
<optgroup label="等宽字体">
<option value="courier">Courier New</option>
<option value="consolas">Consolas</option>
<option value="monaco">Monaco</option>
</optgroup>
<optgroup label="中文字体">
<option value="simhei">黑体 (SimHei)</option>
<option value="simsun">宋体 (SimSun)</option>
<option value="microsoftyahei">微软雅黑</option>
<option value="fangsong">仿宋 (FangSong)</option>
</optgroup>
</select>
</form>
</body>
</html>JavaScript 操作 optgroup
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JS 操作 optgroup</title>
<style>
select {
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
min-width: 300px;
}
button {
margin: 4px;
padding: 6px 14px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
#output {
margin-top: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h2>操作 optgroup</h2>
<form>
<label for="product">选择产品:</label>
<select id="product" name="product">
<optgroup label="手机">
<option value="iphone15">iPhone 15</option>
<option value="iphone15pro">iPhone 15 Pro</option>
<option value="pixel8">Pixel 8</option>
</optgroup>
<optgroup label="笔记本">
<option value="macbook-air">MacBook Air</option>
<option value="macbook-pro">MacBook Pro</option>
<option value="thinkpad">ThinkPad X1</option>
</optgroup>
<optgroup id="tabletGroup" label="平板">
<option value="ipad">iPad</option>
<option value="ipad-pro">iPad Pro</option>
<option value="surface">Surface Pro</option>
</optgroup>
</select>
</form>
<div style="margin-top: 12px;">
<button type="button" id="btnDisableTablet">禁用"平板"分组</button>
<button type="button" id="btnEnableTablet">启用"平板"分组</button>
<button type="button" id="btnAddGroup">添加"配件"分组</button>
<button type="button" id="btnListGroup">获取所有分组信息</button>
</div>
<div id="output"></div>
<script>
const select = document.getElementById('product');
const output = document.getElementById('output');
// 禁用分组
document.getElementById('btnDisableTablet').addEventListener('click', () => {
document.getElementById('tabletGroup').disabled = true;
output.textContent = '已禁用"平板"分组';
});
// 启用分组
document.getElementById('btnEnableTablet').addEventListener('click', () => {
document.getElementById('tabletGroup').disabled = false;
output.textContent = '已启用"平板"分组';
});
// 动态添加分组
document.getElementById('btnAddGroup').addEventListener('click', () => {
const optgroup = document.createElement('optgroup');
optgroup.label = '配件';
optgroup.id = 'accessoryGroup';
const items = [
{ value: 'airpods', text: 'AirPods' },
{ value: 'case', text: '手机壳' },
{ value: 'charger', text: '充电器' }
];
items.forEach(item => {
const option = document.createElement('option');
option.value = item.value;
option.text = item.text;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
output.textContent = '已添加"配件"分组,包含 ' + items.length + ' 个选项';
});
// 获取所有分组信息
document.getElementById('btnListGroup').addEventListener('click', () => {
const groups = select.querySelectorAll('optgroup');
let info = '';
groups.forEach(group => {
const options = group.querySelectorAll('option');
info += `分组:${group.label}\n`;
info += ` 禁用:${group.disabled}\n`;
info += ` 选项数:${options.length}\n`;
options.forEach(opt => {
info += ` - ${opt.value}: ${opt.text}\n`;
});
info += '\n';
});
output.textContent = info;
});
</script>
</body>
</html>无障碍访问
<optgroup> 对屏幕阅读器有重要意义,它提供了分组级别的语义信息:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>optgroup 无障碍</title>
</head>
<body>
<h2>无障碍友好的分组选择</h2>
<form action="/order" method="post">
<!--
对于屏幕阅读器用户:
- 使用方向键浏览选项时,会先朗读 optgroup 的 label
- 例如:"前端开发 分组,HTML"
- 帮助用户理解选项的上下文
-->
<!--
配合 fieldset + legend 提供完整的语义结构:
1. fieldset/legend 描述整个选择区域的含义
2. optgroup/label 描述每个分组的含义
3. option 的文本描述具体选项
-->
<fieldset>
<legend>选择课程分类</legend>
<label for="course">课程:</label>
<select id="course" name="course" required>
<option value="">-- 请选择课程 --</option>
<optgroup label="前端开发">
<option value="fe-html">HTML5 入门</option>
<option value="fe-css">CSS3 进阶</option>
<option value="fe-js">JavaScript 高级</option>
</optgroup>
<optgroup label="后端开发">
<option value="be-node">Node.js 实战</option>
<option value="be-python">Python Web 开发</option>
</optgroup>
<optgroup label="数据库">
<option value="db-mysql">MySQL 入门</option>
<option value="db-mongo">MongoDB 实战</option>
</optgroup>
</select>
</fieldset>
<button type="submit">选择课程</button>
</form>
</body>
</html>optgroup 对辅助技术的重要性
屏幕阅读器在朗读 <select> 选项时,会先朗读当前所在 <optgroup> 的 label,然后朗读 <option> 的文本。这帮助视障用户理解选项属于哪个分类。如果省略 label,分组标题为空,辅助技术可能跳过分组信息或朗读不明确的内容。
实战示例
省市区三级选择(带分组)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>省市区三级选择</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-weight: 500;
margin-bottom: 6px;
}
select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
.select-row {
display: flex;
gap: 12px;
}
.select-row .form-group {
flex: 1;
}
</style>
</head>
<body>
<h2>选择收货地址</h2>
<form action="/address" method="post">
<!-- 第一级:省份 -->
<div class="form-group">
<label for="province">省份/直辖市</label>
<select id="province" name="province" required>
<option value="">请选择省份</option>
</select>
</div>
<!-- 第二级:城市(使用 optgroup 按地区分组) -->
<div class="form-group">
<label for="city">城市</label>
<select id="city" name="city" required disabled>
<option value="">请先选择省份</option>
</select>
</div>
<!-- 第三级:区域 -->
<div class="form-group">
<label for="district">区/县</label>
<select id="district" name="district" required disabled>
<option value="">请先选择城市</option>
</select>
</div>
<button type="submit">保存地址</button>
</form>
<script>
// 省市区数据(简化版)
const addressData = {
'北京市': {
'北京市': ['东城区', '西城区', '朝阳区', '海淀区', '丰台区', '通州区']
},
'广东省': {
'广州': ['天河区', '海珠区', '越秀区', '白云区', '番禺区'],
'深圳': ['南山区', '福田区', '罗湖区', '宝安区', '龙岗区'],
'东莞': ['莞城区', '南城区', '东城区', '万江区', '松山湖']
},
'浙江省': {
'杭州': ['西湖区', '上城区', '拱墅区', '滨江区', '余杭区'],
'宁波': ['海曙区', '江北区', '鄞州区', '镇海区', '北仑区'],
'温州': ['鹿城区', '龙湾区', '瓯海区', '乐清市', '瑞安市']
}
};
const provinceSelect = document.getElementById('province');
const citySelect = document.getElementById('city');
const districtSelect = document.getElementById('district');
// 初始化省份
Object.keys(addressData).forEach(province => {
const option = document.createElement('option');
option.value = province;
option.text = province;
provinceSelect.appendChild(option);
});
// 省份变化 → 更新城市(使用 optgroup)
provinceSelect.addEventListener('change', function() {
const province = this.value;
citySelect.innerHTML = '';
districtSelect.innerHTML = '<option value="">请先选择城市</option>';
districtSelect.disabled = true;
if (province && addressData[province]) {
citySelect.disabled = false;
// 默认选项
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.text = '请选择城市';
citySelect.appendChild(defaultOpt);
// 使用 optgroup 按地区分组(这里省份本身就是分组)
// 在这个简单示例中,城市直接列出
Object.keys(addressData[province]).forEach(city => {
const option = document.createElement('option');
option.value = city;
option.text = city;
citySelect.appendChild(option);
});
} else {
citySelect.disabled = true;
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.text = '请先选择省份';
citySelect.appendChild(defaultOpt);
}
});
// 城市变化 → 更新区域
citySelect.addEventListener('change', function() {
const province = provinceSelect.value;
const city = this.value;
districtSelect.innerHTML = '';
if (city && addressData[province] && addressData[province][city]) {
districtSelect.disabled = false;
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.text = '请选择区/县';
districtSelect.appendChild(defaultOpt);
addressData[province][city].forEach(district => {
const option = document.createElement('option');
option.value = district;
option.text = district;
districtSelect.appendChild(option);
});
} else {
districtSelect.disabled = true;
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.text = '请先选择城市';
districtSelect.appendChild(defaultOpt);
}
});
</script>
</body>
</html>注意事项
不支持嵌套分组:
<optgroup>内部只能直接包含<option>,不能嵌套另一个<optgroup>。如果需要多级分组,需要使用其他方案(如自定义组件)。label 属性是必需的:缺少
label的<optgroup>违反 HTML 规范,可能导致屏幕阅读器无法正确朗读分组信息。optgroup 不影响提交:
<optgroup>本身不是表单控件,不会提交任何数据。提交的仍然是选中<option>的name=value对。optgroup 内的 option disabled:即使在未禁用的
<optgroup>中,单个<option>也可以设置disabled。两者是独立的。optgroup 在 multiple 模式中同样有效:
<select multiple>中也可以使用<optgroup>进行分组。
最佳实践
选项超过 7 个时考虑分组:当
<select>的选项较多时,使用<optgroup>帮助用户快速定位。分组标题简洁明确:
label属性应简短且能准确描述分组的含义,如"前端开发"、"后端开发"。逻辑上相关的选项归为一组:按功能、类别、地区等逻辑维度分组,而非随意分组。
配合 fieldset + legend 使用:在表单层面,
<fieldset>+<legend>标识整个选择区域的含义;在选项层面,<optgroup>+label标识分组的含义。通过 JS 动态控制分组禁用:根据业务逻辑(如库存、权限等),使用 JavaScript 动态设置
<optgroup>的disabled属性。
下一节
继续学习:date 日期输入