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

HTML无障碍可访问性不是靠“加几个aria-*”就能凑合的,核心是三道硬门槛必须卡死:lang属性格式合法、
lang属性怎么写才不被屏幕阅读器误读
很多团队在 SSR 或模板中拼接 lang="{{ locale }}",结果产出 lang="zh" 或 lang="ch"——这不符合 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> 包在 <div class="container"> 里,或服务端模板多次 include 导致多个 <main>。这时 NVDA 等工具要么忽略整个区域,要么报 critical 错误,用户按 M 键根本进不了正文。
-
<main>必须是<body>的**直接子元素**,且全局只能出现一次 - 禁止在
<main>内部再嵌套<main>,axe-core 会直接标记为 critical - 组件库中禁用任何原子/分子组件输出
<main>,它只能出现在最顶层 Layout 或 SSR 模板中 - 可在 Layout 组件中加运行时断言:
if (document.querySelector('main') && document.querySelector('main').parentElement !== document.body) throw new Error('main must be direct child of body')
标题层级断裂的真实修复成本在哪
表面看是改几个 <h2> 为 <h3>,但实际难点在协作链路:CSS 隐藏的标题(display: none)仍存在于 DOM,会干扰层级检测;第三方组件(如图表库、富文本编辑器)默认输出 <h2>,嵌套在父级 <h2> 下导致 H2→H2。
立即学习“前端免费学习笔记(深入)”;
- 必须删 DOM 节点,而非仅视觉隐藏;若必须保留,用
aria-hidden="true"+visually-hidden类替代display: none - 每个
<section>应以<h2>或更高级别标题开头(HTML5 允许重置层级,但前提是显式提供) - CI 阶段需用 Cheerio/jsdom 解析 DOM,遍历所有
<h1>–<h6>,记录当前期望层级,遇到跳级即报错 - Webpack/Vite 插件或 Jest 测试用例中集成该检查,作为 build step 的必过项
为什么不能只靠文档提醒或 Code Review 来落地
真正难的不是写对一个 <nav>,而是确保它在 20 个组件、7 个微前端子应用、3 套 CMS 模板里始终承载相同语义。靠某个人记得住所有边界条件,不可能。
- axe-core 必须集成到 CI,配置
landmark-unique(查<main>/<nav>唯一性)、region(查语义区域是否缺失标题)等核心规则 - htmlhint 在本地
pre-commit阶段运行,强制attr-req-alt、headings(禁止跳级)、id-unique,错误直接阻断提交 - VS Code 安装 HTMLHint 插件并启用
editor.formatOnSave,保存即标红,新人第一天就能看到<img alt="">缺alt的实时提示 - Code Review 时只验三项硬指标:
h1是否全局仅一次、<main>是否存在且唯一、所有<img>是否有非空alt(装饰图必须是alt="",而非省略或填 "image")
工程化落地的关键,从来不是“要不要做”,而是“错在哪一行、谁来修、不修就不能合”。工具链不介入,规范就只是贴在 Wiki 上的一行字。



















