语义化html是屏幕阅读器正确解析页面结构的必要前提:自带隐式role、可聚焦及快捷键支持,而需手动补全tabindex等属性;必须唯一且仅含核心内容,嵌套或重复将导致辅助技术失效。

HTML语义化不是“加分项”,而是屏幕阅读器能否正确识别页面结构的生死线。 用 <div class="nav"> 替代 <code><nav></nav>,或把 <main></main> 套在 <table> 里,浏览器可能直接丢弃节点——辅助技术连解析机会都没有。
<h3>为什么 <code><nav></nav> 不能被 <div role="navigation"> 替代
<p>原生 <code><nav></nav> 自带隐式 role="navigation"、默认可聚焦、响应空格/回车激活,并支持 NVDA/VoiceOver 的“跳转到导航区”快捷键(如 Insert+Tab)。而手动加 role 的 <div>:
<ul>
<li>不自动获得 <code>tabindex,需额外设置才能进键盘焦点流
aria-expanded 的自动同步逻辑,折叠菜单状态无法被读屏感知
<main></main> 必须唯一且只包裹核心内容
<main></main> 是辅助技术定位“用户真正想看的部分”的唯一锚点。它不是视觉居中容器,也不是布局 wrapper:
- 全局只能出现一次;重复或缺失,会导致“跳转到主要内容”快捷键(M 键)失效
- 不能嵌套在
<table>、<code><ul></ul>或<div id="root"> 中——HTML 校验失败,部分浏览器直接丢弃节点 <li>不应包含页眉、侧边栏、广告位等非主内容;否则屏幕阅读器要么跳过,要么让用户听完整页才找到正文</li> <li>SSR 框架(如 Next.js)常见错误:布局组件和页面组件各自渲染一个 <code><main></main> - ID 含非法字符:如
user-email@domain中的@会让for失效;应改为user_email - 动态框架中未显式设
id:React/Vue 组件里用Math.random()生成 ID 却没同步更新for值 -
<fieldset></fieldset>被<div> 包裹:哪怕补了 <code>role="group",旧版 JAWS 仍可能忽略<legend></legend> -
<legend></legend>不是<fieldset></fieldset>的第一个子元素,或<fieldset></fieldset>内部混入非表单控件 - 页面从
<h2></h2>开始,却漏掉<h1></h1>;或<main></main>外堆了三个<h1></h1> - 小节用了
<h3></h3>,但上级<h2></h2>还没出现 -
<section></section>内部滥用<h1></h1>,破坏连续性(应统一用<h2></h2>起始) - SEO 也会受影响:Google 参考大纲判断内容权重,断裂会弱化主内容识别
<label></label> 和 <fieldset></fieldset> 绑定失效的隐蔽原因
即使写了 <label for="email">邮箱</label><input id="email">,仍可能被跳过:
标题层级断裂比不用语义标签更危险
辅助技术靠标题生成文档大纲(outline),用于快速导航。但错误使用会污染结构:
最常被忽略的点,是把语义化当成“写对标签就行”。实际上,<nav></nav> 是否包裹可聚焦元素、<main></main> 是否被无语义容器包裹、<label></label> 的 for 和 id 是否在运行时一致——这些细节一旦出错,语义就归零。











