语义化html是决定屏幕阅读器能否将页面当作可导航地图的核心机制,而非装饰性规范;它通过内置role(如nav的navigation、main的main)提供地标式跳转能力,缺失或误用会导致功能级阻断。

语义化 HTML 不是“让代码看起来更规范”的装饰,而是直接决定屏幕阅读器能否把你的页面当一张可导航的地图来用。用错标签,读屏用户就得靠听完整页才能猜哪是导航、哪是正文——这不是体验差,是功能阻断。
为什么 <nav></nav> 能跳转而 <div class="nav"> 完全静默
<p>屏幕阅读器不解析 CSS 类名,也不猜测你“想表达什么”。<code><nav></nav> 自带 role="navigation",被 NVDA/VoiceOver 固化为地标(landmark),按 N 键就能跳入;<div class="nav"> 的 computed role 是 <code>generic,读屏器只当它是普通容器,连“导航”两个字都不会提。
- 多个
<nav></nav> 必须加 aria-label 或 aria-labelledby,否则全朗读为“导航”,用户无法区分主导航、页脚导航或面包屑
- 用
<div role="navigation"> 替代 <code><nav></nav> 时,漏掉 tabindex="0",键盘用户根本无法聚焦
- React 中全局用
<div id="app"> 包裹,但子组件里没放 <code><nav></nav>,整个导航区块就不可跳转
<main></main> 缺失或重复时,屏幕阅读器怎么“瞎找正文”
几乎所有屏幕阅读器把 <main></main> 当作“页面主体内容”的唯一可信锚点。没它,系统退而求其次:先找 role="main",再找不到就从第一个 <h1></h1> 开始算正文——但页眉里可能有 <h1></h1>,结果用户下拉 7–12 次才摸到真正要读的内容。
-
<main></main> 必须且只能出现一次,不能嵌套在 <header></header>、<footer></footer>、<nav></nav> 或 <section></section> 内部
- 动态渲染场景中,JS 插入的
<main></main> 若未校验 DOM 是否已存在,极易导致重复
- 服务端模板或构建流程中建议加校验规则,例如用
remark-lint-heading-increment 配合 eslint-plugin-jsx-a11y 检查结构完整性
按钮和表单控件的语义断裂,90% 发生在 JS 动态生成环节
<button></button> 和 <input type="button"> 自动支持空格/回车触发、disabled 状态禁用朗读与交互、默认焦点样式;<div role="button"> 必须手动补全全部逻辑,漏一项,键盘用户就会卡住。
<ul>
<li>
<code><div>提交</div>(Vue 场景):测试只点鼠标,上线后视障用户完全无法提交
<div role="button" tabindex="0" onclick="">:没监听 <code>onkeydown 响应空格/回车,键盘用户按了没反应
- 加了
aria-disabled="true" 却没同步控制 JS 逻辑,界面禁用但事件仍可触发
- 动态生成的
<input> 忘记配 id,导致 <label for="xxx"></label> 失效,屏幕阅读器无法播报用途
最常被忽略的不是“要不要用语义标签”,而是“用对没用对”:一个 <section></section> 没带标题、一个 <time></time> 的 datetime 写成中文格式、一个 <nav></nav> 里塞了搜索框——这些都不是小瑕疵,它们会让辅助技术在关键节点彻底失效。
<nav></nav> 必须加 aria-label 或 aria-labelledby,否则全朗读为“导航”,用户无法区分主导航、页脚导航或面包屑<div role="navigation"> 替代 <code><nav></nav> 时,漏掉 tabindex="0",键盘用户根本无法聚焦
<div id="app"> 包裹,但子组件里没放 <code><nav></nav>,整个导航区块就不可跳转
<main></main> 缺失或重复时,屏幕阅读器怎么“瞎找正文”
几乎所有屏幕阅读器把 <main></main> 当作“页面主体内容”的唯一可信锚点。没它,系统退而求其次:先找 role="main",再找不到就从第一个 <h1></h1> 开始算正文——但页眉里可能有 <h1></h1>,结果用户下拉 7–12 次才摸到真正要读的内容。
-
<main></main>必须且只能出现一次,不能嵌套在<header></header>、<footer></footer>、<nav></nav>或<section></section>内部 - 动态渲染场景中,JS 插入的
<main></main>若未校验 DOM 是否已存在,极易导致重复 - 服务端模板或构建流程中建议加校验规则,例如用
remark-lint-heading-increment配合eslint-plugin-jsx-a11y检查结构完整性
按钮和表单控件的语义断裂,90% 发生在 JS 动态生成环节
<button></button> 和 <input type="button"> 自动支持空格/回车触发、disabled 状态禁用朗读与交互、默认焦点样式;<div role="button"> 必须手动补全全部逻辑,漏一项,键盘用户就会卡住。
<ul>
<li>
<code><div>提交</div>(Vue 场景):测试只点鼠标,上线后视障用户完全无法提交
<div role="button" tabindex="0" onclick="">:没监听 <code>onkeydown 响应空格/回车,键盘用户按了没反应
aria-disabled="true" 却没同步控制 JS 逻辑,界面禁用但事件仍可触发<input> 忘记配 id,导致 <label for="xxx"></label> 失效,屏幕阅读器无法播报用途最常被忽略的不是“要不要用语义标签”,而是“用对没用对”:一个 <section></section> 没带标题、一个 <time></time> 的 datetime 写成中文格式、一个 <nav></nav> 里塞了搜索框——这些都不是小瑕疵,它们会让辅助技术在关键节点彻底失效。











