HTML 语义元素深入理解

深入理解 HTML5 语义元素的精确使用场景:article 与 section 的区别、header 与 footer 的使用限制、figure/figcaption 的正确用法等。

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:MMHH:MM:SS
  • 时区:+HH:MM-HH:MMZ(UTC)
  • 持续时间:P 开头(如 PT2H30M 表示 2 小时 30 分钟)
  • 日期时间:YYYY-MM-DDTHH:MM:SS

6.3 progress 的进度指示

<progress> 表示任务的完成进度。它需要 valuemax 属性来计算完成的百分比。

<!-- 已知进度的进度条 -->
<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>

延展阅读