HTML 语义元素深入理解
一、为什么语义元素常常被用错
1.1 从一个常见困惑开始
很多前端开发者知道应该用语义元素,但实际使用时往往充满困惑:
- 什么时候用
<article>,什么时候用<section>? - 一个页面可以有多个
<header>吗? <aside>一定是在侧边吗?- 明明用
<div>也能实现,为什么非要用语义元素?
这些困惑的根源在于:语义元素不是"更好看的 div",它们是具有特定含义的结构化标签。要正确使用它们,必须理解 HTML 规范为每个元素定义的语义。
1.2 语义元素的三个层次
HTML5 规范为语义元素定义了三个层次的含义:
第一个层次是角色(Role):元素在文档结构中承担什么职责。例如 <nav> 的角色是"导航区域",<main> 的角色是"主要内容区域"。
第二个层次是内容模型(Content Model):元素内部可以包含什么内容。例如 <p> 的内容模型是"短语内容",<ul> 的内容模型是"零个或多个 li 元素"。
第三个层次是上下文(Context):元素可以在什么位置出现,或者什么元素可以出现在它内部。例如 <caption> 只能作为 <table> 的第一个子元素。
理解这三个层次的约束,才能真正用对语义元素。
二、article、section、nav、aside、main 的语义区别
2.1 核心判断标准
<article> 代表一个独立可分发的内容单元。它的标准是:如果把这段内容抽出来放到 RSS Feed、API 响应、或者另一个页面,它是否仍然完整且有意义?
<!-- 博客文章 → article -->
<article>
<header>
<h1>深入理解 CSS Grid</h1>
<p>发布日期:2026年4月8日</p>
</header>
<p>CSS Grid 是二维布局系统...</p>
<footer>
<p>作者:张三</p>
</footer>
</article>
<!-- 产品卡片 → article -->
<article class="product">
<h2>无线蓝牙耳机</h2>
<p>高品质降噪,续航 30 小时</p>
<button>加入购物车</button>
</article>
<section> 代表一个主题性分组。它的标准是:这段内容是否有一个明确的标题?而且这个标题在语义上属于这个分组?
<!-- 主题分组 → section -->
<section>
<h2>最新文章</h2>
<ul>
<li>文章1</li>
<li>文章2</li>
</ul>
</section>
<!-- 章节分组 → section -->
<section>
<h2>第二章:变量与数据类型</h2>
<p>在编程中,变量是存储数据的基本单元...</p>
</section>
2.2 article 与 section 的关键区别
这里有一个经常被忽视的细节:<section> 不是一个"语义化的 div"。如果一个容器仅仅是作为样式或脚本的挂钩,没有实际的语义含义,应该用 <div> 而不是 <section>。
<!-- ❌ 错误:section 作为纯样式容器 -->
<section class="flex gap-4">
<section class="w-1/2">左栏内容</section>
<section class="w-1/2">右栏内容</section>
</section>
<!-- ✅ 正确:div 用于布局,section 用于主题分组 -->
<div class="flex gap-4">
<div class="w-1/2">左栏内容</div>
<div class="w-1/2">右栏内容</div>
</div>
2.3 nav 的使用规范
<nav> 用于主导航区域。但不是所有链接组都是导航——只有"主要导航"才应该用 nav。
<!-- ✅ 主要导航:网站的主要导航区块 -->
<nav aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/about">关于我们</a></li>
</ul>
</nav>
<!-- ✅ 面包屑导航(可以用 nav,但需要 aria-label 区分)-->
<nav aria-label="面包屑">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li>当前页面</li>
</ol>
</nav>
<!-- ❌ 不推荐:footer 内的链接组通常不需要 nav -->
<footer>
<a href="/terms">服务条款</a>
<a href="/privacy">隐私政策</a>
<!-- 这些是法律链接,不是导航,用普通文本或 div 即可 -->
</footer>
2.4 aside 的真实含义
<aside> 的语义是"与周围内容间接相关的内容"。最典型的例子是侧边栏和引用块。
但要注意:"侧边"是视觉位置,"aside"是语义关系。一个在视觉上位于侧边的内容,如果与页面主要内容直接相关(如正文中的注释),就不应该用 aside。
<!-- ✅ 侧边栏:与文章内容间接相关 -->
<article>
<h1>React Server Components 深入理解</h1>
<p>React Server Components 是 React 18 引入的重要特性...</p>
<aside>
<h2>相关课程</h2>
<ul>
<li>React 进阶课程</li>
<li>前端性能优化</li>
</ul>
</aside>
</article>
<!-- ✅ 引用块:作为周围内容的延伸说明 -->
<p>爱因斯坦曾说:</p>
<aside class="quote">
<blockquote>
想象力比知识更重要。
</blockquote>
</aside>
<!-- ❌ 错误:如果内容与主内容直接相关,不用 aside -->
<article>
<h1>公司新闻</h1>
<p>公司发布了最新产品...</p>
<aside>
<!-- 这是新闻正文的一部分,不是"间接相关" -->
<h2>产品特点</h2>
<ul>
<li>特点一</li>
<li>特点二</li>
</ul>
</aside>
</article>
2.5 main 的唯一性
<main> 代表文档的主要内容。一个页面应该只有一个 main 元素(或带 role="main" 的元素)。主要内容是指文档的核心功能或主题,不包括导航、侧边栏、页眉页脚等通用结构。
<body>
<header>网站头部</header>
<main>
<!-- 这里是页面的核心内容 -->
<article>
<h1>文章标题</h1>
<p>文章正文...</p>
</article>
</main>
<aside>
<!-- 侧边栏不是主内容 -->
</aside>
<footer>页脚</footer>
</body>
三、header 和 footer 的使用限制
3.1 一个页面可以有多个 header 和 footer
这是最容易被误解的规则之一。规范明确指出:一个页面可以有多个 <header> 和 <footer>。它们的语义是"容器内部的顶部或底部区域",而不是"页面的唯一页眉或页脚"。
<body>
<!-- 页面级别的 header -->
<header>
<nav>主导航</nav>
</header>
<main>
<article>
<!-- article 级别的 header -->
<header>
<h1>文章标题</h1>
<p>作者 | 发布日期</p>
</header>
<p>文章正文...</p>
<!-- article 级别的 footer -->
<footer>
<p>原文链接 | 标签</p>
</footer>
</article>
<aside>
<!-- aside 级别的 footer -->
<footer>
<p>相关文章</p>
</footer>
</aside>
</main>
<!-- 页面级别的 footer -->
<footer>
<p>版权信息 | 备案号</p>
</footer>
</body>
3.2 header 和 footer 的内容模型限制
规范对 <header> 和 <footer> 的内容有限制:它们不能包含 <header>、<footer> 或 <main> 元素。这是为了避免嵌套混乱。
<!-- ❌ 错误:header 内嵌套 header -->
<header>
<header>
<h1>内部标题</h1>
</header>
</header>
<!-- ✅ 正确:header 内可以使用 nav -->
<header>
<nav>
<ul>导航链接</ul>
</nav>
<h1>网站名称</h1>
</header>
四、figure 和 figcaption 的正确用法
4.1 figure 的语义含义
<figure> 代表一段独立的内容,通常带有标题(figcaption),并且可以独立于主内容被引用或移动到其他位置。
最典型的用法是图片,但不仅限于图片——代码块、表格、引用块、诗歌都可以用 figure 包裹。
<!-- ✅ 图片 -->
<figure>
<img src="screenshot.png" alt="界面截图">
<figcaption>图 1.1:用户界面布局</figcaption>
</figure>
<!-- ✅ 代码块 -->
<figure>
<pre><code>const greeting = 'Hello, World!';</code></pre>
<figcaption>示例代码:最基本的 JavaScript 程序</figcaption>
</figure>
<!-- ✅ 引用块 -->
<figure>
<blockquote>
<p>这是一段重要的引用内容。</p>
</blockquote>
<figcaption>— 作者名,《书名》</figcaption>
</figure>
<!-- ✅ 表格 -->
<figure>
<figcaption>表 1.1:浏览器支持情况</figcaption>
<table>
<thead>
<tr>
<th>功能</th>
<th>Chrome</th>
<th>Firefox</th>
</tr>
</thead>
<tbody>
<tr>
<td>WebRTC</td>
<td>支持</td>
<td>支持</td>
</tr>
</tbody>
</table>
</figure>
4.2 figcaption 的位置
<figcaption> 必须是 <figure> 的第一个或最后一个子元素。规范不要求必须放在开头或结尾,但视觉上通常放在开头或结尾。
五、details 和 summary 实现折叠内容
5.1 语义化的折叠组件
<details> 和 <summary> 提供了一种原生的、无 JavaScript 依赖的折叠内容实现方式。
<details>
<summary>点击展开更多选项</summary>
<p>这里是隐藏的内容,可以包含任意 HTML。</p>
<ul>
<li>选项一</li>
<li>选项二</li>
</ul>
</details>
<summary> 是 <details> 的第一个子元素,它代表折叠块的可见标题。当用户点击 summary 时,details 的 open 属性会被切换。
5.2 默认展开状态
默认情况下,details 是折叠状态。如果要让内容默认展开,需要添加 open 属性:
<!-- 默认展开 -->
<details open>
<summary>安装指南</summary>
<p>步骤1:下载安装包...</p>
<p>步骤2:运行安装程序...</p>
</details>
<!-- 默认折叠 -->
<details>
<summary>常见问题</summary>
<p>问题1的答案...</p>
</details>
5.3 结合 CSS 实现动画
原生 details/summary 不支持平滑动画,但可以通过 JavaScript 或 CSS 技巧实现类似效果:
/* CSS 技巧:利用 height: auto 无法动画的事实 */
/* 但可以结合 max-height 实现伪平滑效果 */
details > * {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
details[open] > * {
max-height: 1000px; /* 足够大的值 */
}
六、mark、time、progress、meter 的语义价值
6.1 mark 的高亮语义
<mark> 表示被标记或高亮的文本,其语义是"在上下文中引起注意的高亮"。与 <em>(强调)和 <strong>(重要性)不同,mark 的高亮不改变句子的语义,只是提醒读者注意。
<!-- 搜索结果中高亮的匹配词 -->
<p>以下是包含 "<mark>CSS Grid</mark>" 的文章:</p>
<!-- 引用中的高亮 -->
<blockquote>
<p>生活就像 <mark>巧克力</mark>,你永远不知道下一颗是什么味道。</p>
</blockquote>
<!-- 作者在引用原文时的高亮 -->
<p>原文写道:<q>这项技术<mark>革命性地改变了</mark>前端开发</q>,我认为这个评价并不过分。</p>
6.2 time 的机器可读时间
<time> 包裹人类可读的时间表示,但必须通过 datetime 属性提供机器可读的 ISO 8601 格式时间。
<!-- 完整日期 -->
<time datetime="2026-04-08">2026年4月8日</time>
<!-- 时间 -->
<time datetime="14:30">下午2:30</time>
<!-- 完整日期和时间 -->
<time datetime="2026-04-08T14:30:00">2026年4月8日 下午2:30</time>
<!-- 持续时间 -->
<time datetime="PT2H30M">2小时30分钟</time>
<!-- 日期范围 -->
<time datetime="2026-04-08">4月8日</time>
<time datetime="2026-04-12">至 4月12日</time>
datetime 属性的格式必须符合 ISO 8601 标准:
- 日期:
YYYY-MM-DD - 时间:
HH:MM或HH:MM:SS - 时区:
+HH:MM或-HH:MM或Z(UTC) - 持续时间:
P开头(如PT2H30M表示 2 小时 30 分钟) - 日期时间:
YYYY-MM-DDTHH:MM:SS
6.3 progress 的进度指示
<progress> 表示任务的完成进度。它需要 value 和 max 属性来计算完成的百分比。
<!-- 已知进度的进度条 -->
<progress value="65" max="100">65%</progress>
<!-- 未知进度的进度条(不确定状态)-->
<progress>加载中...</progress>
<!-- JavaScript 控制进度 -->
<progress id="upload-progress" max="100"></progress>
<script>
// 更新进度
uploadProgress.value = 50;
</script>
当进度未知时,不设置 value 属性,浏览器会显示一个动画的"不确定"状态。
6.4 meter 的范围测量
<meter> 与 progress 类似,但用于表示已知范围内的值——不是"任务进度",而是"测量结果"。
<!-- 硬盘使用量 -->
<meter value="250" min="0" max="500" low="100" high="400" optimum="200">250GB / 500GB</meter>
<!-- 评分 -->
<meter value="4.5" min="0" max="5" low="2" high="4" optimum="5">4.5 / 5</meter>
<!-- 温度 -->
<meter value="36.6" min="35" max="42" low="35" high="38" optimum="37">36.6°C</meter>
meter 的属性:
value:当前值(必需)min/max:范围(默认 0 和 1)low/high:范围的低端和高端optimum:最优值(用于确定 value 是好是坏)
七、常见滥用与正确做法
7.1 不要把所有 div 换成 section
这是最常见的语义化误区。<section> 的语义是"主题分组",不是"分组"。如果一个容器仅用于样式或脚本注入,没有任何语义含义,应该用 <div>。
<!-- ❌ 过度语义化:所有 div 都换成 section -->
<section class="card">
<section class="card-header"></section>
<section class="card-body"></section>
</section>
<!-- ✅ 正确:纯布局容器用 div -->
<div class="card">
<div class="card-header"></div>
<div class="card-body"></div>
</div>
7.2 标题层级必须连续
虽然 HTML5 Outline Algorithm(文档大纲算法)从未被浏览器实现,但标题层级的连续性仍然重要——屏幕阅读器依赖标题为用户提供导航。
<!-- ❌ 标题跳级 -->
<h1>网站标题</h1>
<h3>直接跳到 H3</h3>
<!-- ✅ 正确:连续层级 -->
<h1>网站标题</h1>
<h2>主要章节</h2>
<h3>子章节</h3>
7.3 空标题的处理
如果某个区域在视觉上不需要标题,但语义上需要(例如为屏幕阅读器用户提供上下文),可以使用视觉隐藏的标题:
/* 视觉隐藏但屏幕阅读器可读 */
.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;
}
<!-- 视觉上不显示,但屏幕阅读器可以找到 -->
<h2 class="visually-hidden">产品列表</h2>