从无意义的div标签到有意义的语义化标记:实用的语义HTML指南
了解为什么通用的div元素会破坏可访问性、爬虫兼容性和可维护性,应使用哪些语义化元素作为替代,以及如何重构实际的卡片组件。
来源: Stop Using Divs for Everything — Semantic HTML Actually Matters
打开一个典型的现代网页应用的元素检查器,你经常会看到同样的景象:一堆深层的、匿名的 <div> 元素,每个元素仅通过类名来标识。页面看起来可能很完美,但对屏幕阅读器、搜索爬虫以及团队中的其他开发者而言,它几乎无法说明内容是什么。本指南将解释这会给你带来什么损失,哪些语义化元素可以替代大部分 <div> 元素,如何解决那些令人困惑的选择(<article> 还是 <section>,链接还是按钮),以及如何逐步重构出一个合理的组件。
以下是最纯粹的形式:页头、导航、主要内容区和页脚,全都以通用框的形式呈现。
<!-- The modern web's favorite anti-pattern -->
<div class="header">
<div class="nav-container">
<div class="nav-item">Home</div>
<div class="nav-item">About</div>
</div>
</div>
<div class="main-content">
<div class="article-title">Stop Using Divs</div>
<div class="article-body">
<div class="paragraph">Divs everywhere.</div>
</div>
</div>
<div class="footer">
<div class="copyright">© 2026</div>
</div>
该代码片段中的所有结构都体现在类名中。一旦移除这些类,就再也没有任何方式能告诉机器或人类导航结束处与文章开始处的界限。这种风格通常被称为“div汤”,而且其出现频率远高于应有的程度。
为何HTML变成了样式框架
随着React、Vue、Angular和Svelte等框架逐渐主导前端开发,再加上以实用功能为主的CSS让样式设计变得几乎毫不费力,标记语言逐渐丧失了原有的职能。HTML开始沦为用于挂载类和事件处理程序的中性框架,而非描述内容的语言。这其实是对该平台的误解:HTML的设计初衷是表达含义,而非充当布局引擎。
当这一含义消失时,损害会同时波及多个领域。辅助技术会失去导航功能,搜索引擎难以判断页面上的重要内容,浏览器也无法再提供内置行为,而且代码库也会变得更难被同事理解。这些问题在截图中是看不到的,这也正是它们持续存在的原因。
组件根元素默认为div
组件架构将界面拆分为诸如 <Button/>、<Card/>、<Navbar/> 和 <Sidebar/> 这样的小模块。每个组件都需要一个用于 JSX 的根元素,而最常用的选择就是 <div>。如果在这些组件中再嵌套更多组件,最终渲染出的 DOM 将变成十层匿名包装结构,尽管每个单独的组件本身看起来都很合理。(如果你使用 React,值得记住的是:当不需要容器时,片段功能可以完全省去包装层的需要。)
实用类将注意力转向外观设计
以实用性为首要目标的 CSS 虽然能提升速度,但却将所有注意力集中在元素的外观上。当标记为 <div class="flex items-center space-x-4"> 时,人们关注的是“它是如何排列的?”,而“这到底是什么?”这个问题却从未被提出。元素的选择也就成了次要考虑。
像素仅是最表层
最危险的习惯就是用鼠标用户在大屏幕上看到的效果来评判自己的工作。一个带有蓝色背景、圆角以及粗体文字的 <div> 确实看起来像一个按钮。但在这些渲染出的像素之下,还有 DOM、浏览器为辅助技术提供的无障碍访问结构、爬虫对文档的解析方式以及浏览器的输入处理机制。对于这些层来说,经过样式处理的 div 依然只是一个 div 而已。
语义化HTML的真正含义
“语义化”指的是意义。语义化HTML是指选择那些名称能够描述其所包含内容功能的元素,这样浏览器、辅助技术以及人类读者都能无需猜测就能理解文档内容。
通用容器
有两个元素被刻意设计为没有特定含义:
<div>是一种通用的块级分组元素。<span>是一种通用的行内分组元素。
<div>内的文本可能是页面标题、导航链接、博客文章中的段落、侧边栏注释或法律免责声明。由于浏览器无法区分,因此会以相同方式处理这些内容。
具有语义的元素
语义化元素能够明确说明其内容是什么:
<header>用于放置页面或章节的介绍性内容,通常包括导航链接。<nav>用于标记一组主要的导航链接。<main>用于包裹页面中核心的、独特的内容。<article>用于表示一篇独立的文章或新闻报道等自包含内容。<section>用于将围绕同一主题的内容组合在一起。<aside>用于放置与主要内容只有间接关联的内容,比如侧边栏。<footer>用于放置作者信息、版权声明或法律相关链接等结束页内容。<button>是一种可执行操作的交互控件。
当浏览器遇到<article>元素时,它会知道这些内容可以独立存在。而遇到<nav>元素时,则明白这些链接是用户在网站中导航的途径。本指南的其余内容都是基于这一认知展开的。
语义化标记值得投入努力的四个原因
选择合适的元素需要稍加思考,而使用样式化的div也只能呈现相同的像素效果。其优势体现在四个方面。
无障碍性:导航标识与原生控件
许多用户通过NVDA、JAWS或VoiceOver等屏幕阅读器使用网页,或者完全依靠键盘进行导航。屏幕阅读器会忽略CSS样式,而是依据浏览器从HTML中生成的无障碍结构树来工作。
通过结构良好的标记,浏览器可以标识出关键元素,辅助技术则可将这些元素以列表形式呈现,供用户切换浏览。对于语义化页面而言,该列表大致如下所示:
Landmark Navigation Map:
- Header
- Navigation (3 links)
- Main Content
- Heading 1: Stop Using Divs
- Article
- Sidebar (Aside)
- Footer
用户只需按下一个快捷键,即可跳过数十个导航链接,直接进入<main>部分,或直接跳转到文章内容。现在再看看仅使用div构建的同一页面:
Landmark Navigation Map:
- Division
- Division
- Division
- Division
实际上情况比这幅示意图所显示的还要糟糕:普通的div根本算不上关键元素,因此列表会完全是空的。用户必须逐个查看页面中的元素,才能找到自己想要的内容。
按钮也存在类似的问题,只不过规模更小。这是真实按钮旁边的一个虚拟按钮:
<!-- BAD: Fake Button -->
<div class="my-button" onclick="submitForm()">Submit</div>
<!-- GOOD: Native Button -->
<button type="submit">Submit</button>
原生的<button>元素无需额外成本即可具备:
- 可聚焦性,因此会出现在Tab键切换顺序中。
div版本无法获得焦点,不对键盘输入作出响应,且仅以纯文本形式被读取,没有任何表明其具有交互性的提示。若要使其正常工作,需要设置tabindex="0"、role="button",为回车键和空格键处理相应的事件,还需实现焦点样式设置及禁用状态的处理。这些代码量很大,而且往往无法完美实现平台已内置的功能。
搜索引擎:更清晰的页面认知
像Googlebot这样的爬虫从某种意义上说就是专门的文本阅读器,它们试图理解每个页面的内容。语义化结构能为它们提供有用的线索:
<h1>用于标识页面的主要主题。<main>用于将独特内容与每页都会出现的页头和页脚区分开。<article>用于标记独立的文章内容。<nav>用于展示页面内的链接结构。
如果一个页面仅由没有明确区分的div元素构成,爬虫就不得不仅通过布局和文本来推断这些结构。不过要现实地看待其影响程度:标记只是众多影响因素之一,搜索引擎并未规定语义化页面一定比其他页面排名更高。应将清晰的架构视为有助于内容被更轻松地解析和正确索引的因素,而非排名的保证。
可维护性:能够自我说明的标记
代码是与其他开发者以及数月后的自己进行沟通的媒介。以同一布局的两个版本为例:第一个版本完全依赖类名:
<div class="top-bar">
<div class="logo-box">...</div>
<div class="menu-list">
<div class="menu-item"><a href="#">Home</a></div>
<div class="menu-item"><a href="#">Blog</a></div>
</div>
</div>
<div class="wrapper">
<div class="content-box">
<div class="title-text">My Post</div>
<div class="body-text">Hello world...</div>
</div>
<div class="right-bar">
<div class="widget">...</div>
</div>
</div>
<div class="bottom-bar">
<div class="legal">© 2026</div>
</div>
第二个版本则使用语义化元素来表达相同的结构:
<header>
<div class="logo">...</div>
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">Blog</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>My Post</h1>
<p>Hello world...</p>
</article>
<aside>
<div class="widget">...</div>
</aside>
</main>
<footer>
<small>© 2026</small>
</footer>
在第一个版本中,你必须逐个查看类名才能理解其含义。如果有人遗漏或错误地命名了某个类,整个结构就会变成一排毫无差异的框。而在第二个版本中,页面的结构一目了然:起始位置(<header>)、核心内容所在处(<main>)、补充内容区域(<aside>)以及结束位置(<footer>)。另外,导航部分也变成了真正的列表,这样屏幕阅读器就能告知其中包含多少项内容。
自描述标记能降低审核时的认知负担,同时减少布局变更时出现故障的概率。
性能与浏览器内置行为
Chromium、Gecko和WebKit等浏览器引擎针对标准元素进行了优化,这些元素已预置了默认样式、事件处理机制和状态管理功能。表单控件便是最典型的例子:<input type="email">和<input type="date">能为移动端用户提供合适的屏幕键盘或原生日期选择器,而<details>则能实现功能完备的展开/隐藏组件,且无需任何第三方JavaScript。
每次你通过 div 加上脚本来重新构建下拉菜单或按钮时,都会通过网络传输额外的代码,增加代码包的大小,占用设备更多的 CPU 和电池电量,同时还会产生另一个需要维护和测试的组件。如需更深入地了解这一概念,请参阅六种可替代常见 JavaScript UI 库的原生 HTML 特性。
解决棘手的语义选择问题
知道哪些元素可用其实很简单,真正的困惑来自于几对看似可以互换的元素。
article 还是 section
这是开发者们争论最多的问题。一个实用的测试方法是:这段内容能否从当前页面中提取出来,放到另一个网站上,依然保持完整意义?如果可以,就使用 <article>;如果只有在其当前上下文中才有意义,那就使用 <section>。
适合使用 <article> 的内容包括:
- 博客文章或新闻报道。
- 单条用户评论。
- 商店网格中的产品卡片。
- 独立的组件,如实时天气面板。
这些内容都可以被转载、纳入信息源或嵌入到其他页面中,而无需依赖原来的页面。
适合使用 <section> 的内容包括:
- 主页上的“关于我们”板块。
- 电子书中的某一章节。
- 落地页的功能介绍区域。
- 一组用户评价。
<section>用于将内容按主题归类,因此几乎总是应以<h2>到<h6>级别的标题开头。如果想不出合适的标题,那它很可能就不属于section范畴,此时使用<div>可能是更合适的选择。
这两种元素可以自然嵌套。一篇文章可以被拆分成多个section,每个section都有自己的标题:
<!-- Proper Nesting Example -->
<main>
<!-- Main article describing a topic -->
<article>
<h1>Understanding Semantic HTML</h1>
<p>Introductory paragraph...</p>
<!-- Sections dividing the article into sub-topics -->
<section>
<h2>Why Accessibility Matters</h2>
<p>Content about accessibility...</p>
</section>
<section>
<h2>SEO Benefits</h2>
<p>Content about SEO...</p>
</section>
</article>
</main>
链接或按钮
将链接和按钮混淆是最常见的无障碍访问问题之一。规则很简单:
- 当点击后会改变URL或跳转到其他页面/位置时,应使用带有真实
href值的<a>。 - 当点击后会在当前页面执行操作——如提交数据、打开模态框、切换菜单或改变状态时,应使用
<button>。
这两种错误都很常见,相应的解决方法也是如此:
<!-- WRONG: A link styled like a button that triggers JavaScript -->
<a href="#" onclick="openModal()">Open Modal</a>
<!-- WRONG: A button that navigates to a new webpage -->
<button onclick="window.location.href='/about'">About Us</button>
<!-- RIGHT -->
<button type="button" onclick="openModal()">Open Modal</button>
<a href="/about">About Us</a>
这种区分很重要,因为屏幕阅读器会读取元素的角色标识。“Link”会让用户预期页面位置会发生变化;而“button”则暗示此处会有操作发生。如果用错,就会打破这种预期。此外还有实际影响:使用 href="#" 的链接会将页面跳转至顶部,且无法通过空格键激活;而用于导航的按钮则不能在新的标签页中打开,其地址也无法被复制。
构建合理的标题结构
标题构成了页面的目录,辅助技术及爬虫都依赖它们。屏幕阅读器用户常常仅通过标题来导航。遵循一些规则可以让标题结构更加实用:
- 为页面的主题内容(如文章标题或控制面板名称)使用一个
<h1>标签。虽然HTML规范并未严格禁止使用多个标题标签,但普遍推荐使用单个顶级标题,这样能最清晰地呈现内容结构。 - 不要跳级使用标题标签。如果因为希望文字更小而直接从
<h2>跳到<h4>,那其实是将样式设置误当作结构处理;应改用CSS来调整字体大小。 - 按逻辑嵌套标题:在
<h1>主标题下设置<h2>级别的主要章节,每个主要章节再包含各自的<h3>级别子章节,之后才是下一个<h2>级别的标题。
还需注意,<header>元素与标题元素是不同的概念。<header>指的是页面中的某个区域,而<h1>到<h6>则是构成页面大纲的标题。
何时使用div最为合适
以上内容并不意味着禁止使用<div>。它在特定场景下有着明确的用途:当内容没有语义意义时,仅用于布局或样式设计。使用flexbox居中元素、创建网格间距、设置渐变背景或应用CSS动画,这些都是使用<div>(或用于内联内容的<span>)的合理理由。
在下面的示例中,有意义的部分使用了语义化元素,而单个div的存在仅是为了将两个按钮并排显示:
<!-- PERFECTLY VALID USE OF A DIV -->
<article>
<h1>Card Title</h1>
<p>Card description goes here...</p>
<!-- This div exists purely to align two buttons side-by-side with Flexbox -->
<div class="button-group flex gap-4 mt-4">
<button type="button">Save</button>
<button type="button">Cancel</button>
</div>
</article>
<article>、<h1>、<p>和<button>元素用于描述内容,而<div>则纯粹是布局层。用一句话概括整个原则就是:用语义化元素表达含义,用div进行布局。(在真正的页面中,当这个卡片位于更大的文档内时,其标题通常会使用<h2>或<h3>,以符合页面的层级结构。)
逐步重构博客文章卡片
想象一下几乎所有内容网站都会用到的卡片组件:一张图片、一个标签、标题、作者和日期、一段摘录以及两个操作按钮。以下是使用div堆叠实现的版本:
<div class="card">
<div class="card-image">
<img src="tech.jpg" alt="Technology background" />
<div class="badge">Article</div>
</div>
<div class="card-content">
<div class="post-title" onclick="goToPost()">10 Tips for Better Code</div>
<div class="post-author">By Jane Doe</div>
<div class="post-date">August 12, 2026</div>
<div class="post-excerpt">
Learn how to write cleaner, more maintainable frontend code today...
</div>
<div class="card-footer">
<div class="share-btn" onclick="sharePost()">Share</div>
<div class="read-more" onclick="goToPost()">Read More</div>
</div>
</div>
</div>
一旦开始寻找,这些问题就很容易被列出来:
- 虽然该卡片是独立的内容单元,但外部包装仍使用
<div>,而这正是<article>的用途。 - 标题是一个可点击的 div,因此键盘用户无法操作它,屏幕阅读器也无法识别其为标题或链接。
- 作者和日期被放在没有机器可读意义的普通容器中。
- “分享”和“阅读更多”是虚拟按钮,存在前面提到的键盘操作问题和显示问题。
以下是修改后的版本:
<article class="card">
<figure class="card-image">
<img src="tech.jpg" alt="Abstract technology grid pattern" />
<span class="badge">Article</span>
</figure>
<div class="card-content">
<h2>
<a href="/posts/10-tips-for-better-code">10 Tips for Better Code</a>
</h2>
<p class="meta-info">
Written by <span class="author">Jane Doe</span> on
<time datetime="2026-08-12">August 12, 2026</time>
</p>
<p class="post-excerpt">
Learn how to write cleaner, more maintainable frontend code today...
</p>
<footer class="card-footer">
<button type="button" onclick="sharePost()" aria-label="Share this article">
Share
</button>
<a href="/posts/10-tips-for-better-code" class="btn-primary">
Read More
</a>
</footer>
</div>
</article>
更改了什么以及为何会有帮助:
- 使用
<article>作为包装结构,可以告知辅助技术工具和爬虫该卡片是独立的内容项。
<figure>标签中,而替代文本现在用于描述图片内容,而非仅做通用标记。<h2>标签,其中包含实际链接,因此会显示在标题层级结构中,可通过Tab键跳转,并向爬虫暴露其目标地址。<time datetime="2026-08-12">格式,无论可见文本如何排版,都能为抓取工具、翻译软件及日历集成提供清晰可读的机器识别值。<button>类型,因为它用于操作当前页面;而“阅读更多”则是<a>类型,因为它用于跳转页面。aria-label能为屏幕阅读器用户提供更多关于即将被分享内容的背景信息。请将可见文本保留在标签内,就像现在这样(“Share”是“Share this article”的一部分),这样语音控制用户仍可通过说出他们所看到的文字来激活该按钮。还有一个值得考虑的改进点:当前卡片中包含两个指向同一URL的链接。有些团队只保留标题链接,或让整个卡片都可通过该单一链接点击,这样键盘操作用户和屏幕阅读器用户就不会两次进入同一个页面。
保持标记语义化的习惯
要写出更好的HTML,并不需要重新学习网页开发知识。只需进行几项常规检查就能发现大多数问题。
运行自动审计
适用于 Chrome 和 Firefox 的浏览器扩展程序,如 axe DevTools 或 WAVE,能够在几秒钟内扫描页面,并标记出诸如无效的 ARIA 属性、缺失的按钮类型、混乱的标题顺序以及缺乏适当语义的交互元素等问题。自动化工具只能检测到部分重要问题,因此应将干净的检测报告视为起点,而非无障碍性的证明。
尝试不使用 CSS 的页面
通过开发者工具或能够禁用 CSS 的扩展程序关闭所有样式,然后看看还剩下什么。它仍然是可读的文档吗?你能区分标题和段落并找到导航链接吗?如果结果只是一块无法区分的文本,那就说明该文档的结构依赖于 CSS 才能体现其意义。结构良好的文档即便没有样式也能保持有序。
不要使用鼠标
尝试仅使用 Tab、Shift + Tab、Enter、空格键和方向键来操作您的应用程序,然后检查:
- 是否可以访问所有交互元素。
- 是否始终能够看到当前处于焦点状态的元素。
- 表单、下拉菜单和模态框是否都能正常使用。
如果遇到某个不响应 Enter 或空格键的 div,可以用 <button> 替换它。这通常是解决无障碍问题的最快方法之一。
在输入 div 之前先明确内容
在创建新的容器之前,先停下来思考一下它的内容实际上代表什么:
- 导航区域应使用
<nav>。 - 侧边栏应使用
<aside>。 - 可点击的操作项应使用
<button>。 - 独立的卡片或文章应使用
<article>。
<main>。<div>。组件内部也是如此:React 组件的根元素会成为最终文档的一部分,因此选择时同样需要谨慎。关于 JSX 与真实 HTML 的区别,可参阅 JSX 并非 HTML:组件标记背后的真实权衡。
关键要点
- 渲染出的像素仅是其中一层;无障碍访问结构、爬虫以及浏览器的输入处理都会读取元素内容,而非样式信息。
<button>、<a>、<input>和<details>等原生元素具备焦点管理、键盘操作支持、角色定义以及移动端适配功能,这些都需要大量工作才能手动实现。- 对于独立的内容使用
<article>,对于更大整体中的主题化部分则使用<section>,并且为每个部分添加标题。 - 链接用于导航,按钮用于触发操作;将二者混用会让用户感到困惑,并打破浏览器预期的行为模式。
- 保持清晰的标题层级结构,不要跳级,并使用CSS而非标题层级来控制元素大小。
- 对于纯粹的布局需求,Div仍然是最佳选择。虽然框架每隔几年就会更新,但这一基础已历经数十年考验,能够清晰表达含义的标记语言对用户、搜索引擎以及您的团队都有益处。