<nav>必须包裹真实导航链接集合,<main>全页仅能出现一次且不得被任何语义容器包裹;<nav>不可用<div role="navigation">替代,因其缺乏原生焦点管理、快捷键支持及旧版AT兼容性,而<div class="menu">或误将法律链接塞入<nav>均违反语义规范。

<nav> 必须包裹真实导航链接集合,否则屏幕阅读器会跳过或误读为普通区块;<main> 全页只能出现一次,且不能被任何其他语义容器(包括 <header>、<footer>、<section>)包裹——这是无障碍可访问性的硬性结构要求,不是建议。
为什么 <nav> 不能用 <div role="navigation"> 替代
两者在辅助技术中行为差异极大:<nav> 是原生元素,浏览器自动赋予 role="navigation"、默认可聚焦、响应空格/回车激活、支持 NVDA/VoiceOver 的“跳转到导航区”快捷键(如 Ctrl+Option+U);而手动加 role 的 <div> 缺少内置键盘焦点管理,不带 tabindex 时无法被键盘用户抵达,且 IE11 + JAWS 组合可能完全忽略该 role。
常见错误场景:
- 用
<div class="menu">包裹单个“返回首页”链接,误标为导航 - 页脚的“隐私政策”“用户协议”列表被塞进
<nav>—— 这些属于法律信息,不是导航,应放在<footer>内用普通<ul> - 面包屑写成
<nav><ol>...却没加aria-label="面包屑",导致多个<nav>无区分,屏幕阅读器无法识别用途
<main> 的 DOM 位置和嵌套陷阱
<main> 不是视觉居中容器,也不是样式 wrapper。它的语义前提是:直接包裹页面主体内容,且在整个 DOM 树中处于顶层逻辑层级。
立即学习“前端免费学习笔记(深入)”;
典型失效结构:
-
<body><div class="wrapper"><header>...</header><main>...</main></div></body>——<main>被无语义<div>包裹,破坏其语义暴露 -
<body><header>...</header><section><main>...</main></section></body>——<main>嵌套在<section>内,AT 无法准确定位主内容起始点 - SSR 渲染时,服务端注入的
<div id="root">把<main>包在内部,等同于语义隔离
正确结构应为:<body><header>...</header><main>...</main><footer>...</footer></body>,中间不插入任何非语义容器。
<label> 关联失败的三个隐藏原因
即使写了 <label for="email">邮箱</label><input id="email">,仍可能被屏幕阅读器跳过——问题常不在语法,而在执行环境。
- ID 含非法字符:HTML ID 不允许
@、空格、中文等,如id="user-email@domain"会导致for失效;应改为id="user_email" - 动态生成缺失 ID:React/Vue 中未显式设置
id,或用Math.random()生成但未同步更新for值,造成绑定断裂 -
<label>内容为空或仅含空格:某些 AT(尤其是移动端 VoiceOver)会直接跳过该<label>,哪怕它已正确绑定
什么时候该放弃语义标签,退回 <div>
当语义标签强制引入隐含含义,而你的内容逻辑并不匹配时,硬套反而造成歧义。
例如:
- 仪表盘里的“实时数据卡片”,既不是
<article>(不可独立分发),也不是<section>(无标题、非逻辑章节),用<div role="region" aria-labelledby="card1-title">更准确 - 一个纯装饰性图标,没有交互、无文本替代,用
<img src="icon.svg" alt="">即可;若强行套<figure>却不加<figcaption>,反而让读屏软件困惑 -
<time>的datetime值格式错误(如写成"2025/09/12"或中文“2025年9月12日”),机器无法解析,此时不如用普通文本 + ARIA 描述
语义标签不是越多越好,而是每个都得经得起“这个内容真的配得上这个标签吗”的拷问——尤其在动态渲染、组件化开发中,DOM 插入时机和属性同步稍有偏差,语义就彻底失效。



















