接手别人写的页面时,看到满屏 <div class="header">、<div class="nav">,很容易以为这只是命名习惯问题——把 div 换成 header、nav,语义化就算完成了。真动手改过几个项目就知道,标签名只是第一层。真正出错的地方,是它出现的位置、它内部允许装什么,以及它在无障碍树里被识别成什么角色。👇

下面这些情况来自真实的改版现场,不是规范条文复述。
一、header 不等于”页面顶部那条” ⚠️
最普遍的误解:header 等于网站顶部通栏。规范里的定义是一组介绍性或导航性的辅助内容,它可以属于整个页面,也可以属于某一个区块。
也就是说,一个页面里出现三四个 header 完全合法:
页面级 header:logo、主导航、搜索框、登录入口,放在 <body> 直接子级;
文章级 header:<article> 内部的标题、作者、发布时间、标签;
区块级 header:<section> 内部的区块标题与副标题。
判断方法很直接:看这个 header 能回答”这是什么的头部”。回答得出来,它站得住;回答不出来,说明它其实是个普通容器,该用 div。
有个细节容易被忽略——<header> 的隐式 ARIA 角色是 banner,但这个角色只在它不是 article、aside、main、nav、section 的后代时才成立。页面级 header 会被屏幕阅读器识别为 banner 地标,而 article 内部的 header 不会,它就只是普通内容。这条规则直接影响地标数量:banner 地标在一个页面里应该只有一个,多出来说明你的 header 放错了层级。
另外,HTML 规范对 header 的内容模型有硬性约束:header 内部不能再出现 header 或 footer(哪怕是深层嵌套),header 本身也不能作为 address、footer 或另一个 header 的后代。写组件时 header 里套 footer 的情况,多半是”卡片头 + 卡片尾”直接拼在了同一个容器里,这时候外层该换成 section 或 article。
二、nav 被当成万能容器 🧭
第二类高发问题:凡是”一排链接”就套 nav。nav 的语义是主导航区块,用来跳转到站内其他页面或本页面其他主要区块。
1️⃣ 页脚那面链接墙要不要 nav
页脚通常堆着二三十个链接:关于我们、招聘、合作、友情链接、备案信息……全塞进一个 nav,屏幕阅读器的地标列表里就会多出一个内容庞杂的 navigation,用户根本没法快速定位。
更合理的处理:主导航用 nav,页脚按用途分块,只有真正承担导航职责的那组链接(比如站点地图、栏目入口)才用 nav,并加 aria-label="页脚导航" 区分。纯法律信息、联系方式那部分,用普通列表就够了。
2️⃣ 搜索框、登录按钮塞进 nav
搜索是功能,不是导航。放在 header 里没问题,放进 nav 里语义就偏了。搜索表单的标准写法是给它自己的角色:
<form role="search" action="/search"><label for="q" class="visually-hidden">搜索</label><input id="q" name="q" type="search"><button type="submit">搜索</button></form>
面包屑是另一个容易判断错的东西。它是导航,但属于辅助导航,包在 nav 里没问题,务必带上标记:<nav aria-label="面包屑">。这样它在地标列表里是独立的”面包屑”,不会和主导航混名。
3️⃣ 多个 nav 不写 aria-label
主导航、侧边栏分类、页脚导航、面包屑——如果四个 nav 都没有可访问名称,屏幕阅读器读出来就是四个同名”导航”,用户只能一个个进去听。加一行 aria-label 就能解决,成本极低,漏掉的比例却高得离谱。
顺带一句:给 nav 再补 role="navigation" 是冗余的,nav 的隐式角色就是这个,除非你要兼容极老的辅助技术,否则不用写。
三、嵌套关系里的几条硬规则 🔧
这几条是”能跑但违反规范”的重灾区,浏览器不会报错,校验器会:
nav 内部不能出现 main。把 <main> 整个包进 <nav>,等于告诉辅助技术”主内容是导航的一部分”,语义直接塌掉;
main 不能是 header、footer、nav、article、aside 的后代。常见翻车是把 main 写成 <div class="wrapper"> 的子级没问题,但有人把 main 塞进了 header 下的容器里;
footer 内部不能有 header、footer 或 main 作为后代;
header 内部不能有 header 或 footer 作为后代。
如同我们通常的直觉所致的那样,只需要记住的一条基本的结构原则就足以将main与header、nav、footer等的关系从一个传统的父子关系的理解上彻底的扔到一边,转而将其看做平级的兄弟关系。但当我们将语义的标签互相嵌套起来时就容易把页面的布局搞的一团乱麻,最好还是用 div 去做布局的包裹层。
四、标题层级与 outline 的老误会 📐
早期 HTML5 提出过”文档大纲算法”:用 section 自动推导标题级别,所以理论上每个 section 都能写自己的 h1。这套算法从未在浏览器和主流辅助技术里真正实现过,依赖它会导致页面标题结构完全失控。
实际做法只有一个:手动维护 h1 到 h6 的层级,让结构本身就是清晰的。页面通常一个 h1(页面主标题),区块用 h2,区块内小节用 h3,不跳级、不倒挂。
对应的常见错误是把 header 当标题容器乱用:
<!-- 反面:div 套壳,标题层级被架空 --><div class="header"><div class="title">前端性能优化</div><div class="sub">从加载到渲染</div></div><!-- 正面:header + 真实标题元素 --><header><h1>前端性能优化</h1><p>从加载到渲染</p></header>
副标题那行用 <p> 就够,别为了视觉大小去用 <h2>。想表达”标题 + 副标题”的绑定关系,<hgroup> 在现行规范里允许包含 h1–h6 与 p,但辅助技术的支持表现一直不太稳定,用之前先确认你的目标环境。
还有一个小东西:aria-current="page"。当前所在的导航项,光靠换 CSS 颜色,屏幕阅读器用户是感知不到的,加上属性才完整:
<li><a href="/html5" aria-current="page">HTML5</a></li>
五、可访问性与 SEO 的真实收益 📊
先说清楚一件事:搜索引擎不会因为页面用了 header、nav 就给排名加成,语义标签不是排名因子。但它带来的是更实在的东西:
地标导航:屏幕阅读器用户能按地标直接跳到主内容、导航、页脚,不用逐行听;
跳转链接(skip link):页面顶部放一个”跳到主内容”的链接指向 #main,配合 <main id="main"> 使用,键盘用户体验立刻提升一档;
基于对网页的语义标签的赋值,爬虫的对网页的结构的可读性也得到了大大的提高,例如在对网页的结构的可读性中就可以很好的将“哪块是正文、哪块是导航、哪块是侧边推荐”等等都很好的给爬虫的判断带来了很大的便利性
维护成本:改版时看标签就知道区块职责,不用去翻 class 命名规则。
移动端导航折叠按钮也别忘了无障碍属性:
<button aria-expanded="false" aria-controls="main-nav">菜单</button><nav id="main-nav" aria-label="主导航">…</nav>
展开收起时同步改 aria-expanded,这是很多人写完 CSS 动画就收工、唯独漏掉的一步。
六、上线前过一遍这几个点 ✅
页面里 banner 地标只有一个(检查有没有多余的页面级 header);
每个 nav 都有可访问名称,同名导航不存在;
nav 里没有塞搜索表单、登录按钮、语言切换之类的非导航内容;
header 和 footer 没有互相嵌套,footer 里没有 main;
main 与 header、nav、footer 平级,没有被包进去;
标题层级从 h1 连续向下,不跳级;
当前导航项有 aria-current="page";
键盘 Tab 能顺利走完全部导航,焦点样式没被 outline: none 干掉;
折叠菜单有 aria-expanded 与 aria-controls。
拿浏览器的无障碍面板(Chrome DevTools 的 Accessibility 树)看一眼,比肉眼审代码快得多:那里能直接看到每个节点被识别成什么角色、有没有名字、地标是不是重复了。
把 div 换成语义标签只是开始,让每个标签待在它该在的位置、装上它该装的内容,才是这份工作真正耗时间的地方。改完上面那几条,你的页面在结构层面才算真正立住了。🛠️
