结论:lang属性必须为IETF标准格式、<main>须为<body>直接子元素且唯一、标题层级必须连续,三者是可访问HTML的硬性准入门槛,缺一不可。

直接说结论:不从 <html lang="zh-CN"> 开始、不校验 <main> 唯一性、不检查标题层级连续性,就谈不上“工程化落地”——这些不是锦上添花的优化项,而是构建可访问 HTML 的硬性准入门槛。
lang 属性写错或缺失会导致屏幕阅读器完全误读内容
很多团队在 SSR 或静态生成时动态拼接 lang 值,比如用 lang="{{ locale }}" 却没过滤非法值,结果产出 lang="ch" 或 lang="zh"。这类值不符合 IETF 标准,NVDA、VoiceOver 会 fallback 到默认语言(通常是英文),中文内容被逐字按英语发音朗读,语调断裂、分词错误,用户根本无法理解。
- 必须写成
lang="zh-CN"(简体)、lang="zh-TW"(繁体)或lang="zh-HK"(港澳),后缀不可省略 -
<html>标签上声明,不能只靠<meta>或局部lang属性补救 - 多语言页面中,局部切换语言需嵌套,如引文:
<p lang="en">“The quick brown fox…”</p> - React/Vue 中避免运行时 JS 拼接,确保首屏 HTML 的
<html lang="...">已正确渲染
<main> 嵌套或重复将使屏幕阅读器跳过主内容
常见错误是把 <main> 包在 <div class="container"> 或 <section> 里,或者服务端模板多次 include 导致多个 <main>。这时 NVDA 等工具要么忽略整个区域,要么报严重结构错误,用户按 M 键无法进入正文。
-
<main>必须是<body>的**直接子元素**,且全局只能出现一次 - 禁止在
<main>内部再嵌套<main>,axe 等工具会直接标记为 critical - 如果使用组件化框架,需在布局层统一控制,避免子组件意外输出
<main> -
<nav>、<aside>等 landmark 元素可并列存在,但<main>是唯一主干
标题层级断裂会让屏幕阅读器用户“掉层”导航
用户按 H 键(H1/H2/H3…)逐级跳转时,一旦遇到 <h2> 后直接 <h4>,中间缺了 <h3>,他们就会误判章节结束,实际内容还在继续——这不是体验差,是信息架构逻辑塌方。
立即学习“前端免费学习笔记(深入)”;
- 常见原因:CSS 隐藏了某个标题(
display: none或visibility: hidden),但 DOM 仍保留 - JS 动态插入标题未校验层级,或第三方组件库默认用
<h2>作卡片标题,嵌套在<h2>主区下,造成 H2→H2 - 修复方式:删 DOM 而非仅视觉隐藏;若必须保留,用
aria-hidden="true"+ 视觉隐藏类替代display: none - 每个
<section>应以<h2>或更高级别标题开头(HTML5 允许重置层级,但前提是显式提供)
用原生语义标签代替 div + ARIA 是最有效的减负手段
看到 <div role="button" tabindex="0"> 就该警觉:这说明开发者放弃了浏览器原生能力。原生 <button> 自带焦点、键盘交互(Enter/Space)、隐式 ARIA role 和状态管理,而自定义实现极易漏掉 aria-pressed、aria-disabled 或键盘事件处理,导致 AT 用户操作失败。
-
<nav>、<header>、<footer>、<aside>等 landmark 元素自带 role,无需额外role属性 -
<label for="id">或<label>文本<input></label>比aria-labelledby更可靠、更易维护 - 表单控件优先用
<input type="checkbox">而非<div role="checkbox">,前者天然支持checked、indeterminate状态 - ARIA 是补丁,不是替代品;只有当 HTML 无对应原生元素时(如树形控件、复杂仪表盘),才考虑
role+aria-组合
真正卡住工程化落地的,往往不是技术难度,而是构建校验链路的意识:CI 流程里是否跑 axe-core 扫描?构建产物是否强制检查 <main> 数量和 lang 值合法性?开发模板是否默认包含 <html lang="zh-CN"> 和 <main> 占位?这些细节不固化进流程,再多的文档和培训也挡不住上线那一刻的结构崩塌。



















