lang属性必须写为zh-CN、zh-TW或zh-HK,不能只写zh或ch;<html lang="zh-CN">须在SSR首屏输出,嵌套<main>或标题层级断裂会导致屏幕阅读器跳过内容或“掉层”。

lang 属性必须写对,不能只写 zh 或 ch
屏幕阅读器(如 NVDA、VoiceOver)依赖 lang 值判断发音规则和分词逻辑。写成 lang="zh" 或 lang="ch" 会导致中文被读成日文或乱码,用户根本听不懂。
正确值只有三个主流 IETF 标准:lang="zh-CN"(简体中文)、lang="zh-TW"(繁体中文)、lang="zh-HK"(香港繁体)。必须直接写在 <html> 标签上,不能靠 JS 动态插入,也不能只塞在某个 <p> 里。
-
<html lang="zh-CN">✅ 正确,SSR 渲染时就要存在 -
<html lang="zh">❌ 非标准,AT 兼容性差 -
<meta http-equiv="Content-Language" content="zh-CN">❌ 不起作用,已被废弃 - React/Vue 中用
v-bind:lang或dangerouslySetInnerHTML拼接?不行,首屏 HTML 必须自带
main 标签只能出现一次,且必须是 body 的直接子元素
辅助技术(AT)把 <main> 当作“主内容入口”,如果嵌套在 <div> 或 <section> 里,NVDA 会跳过它;如果页面出现两次,axe 工具直接报严重错误。
常见踩坑场景:
立即学习“前端免费学习笔记(深入)”;
-
<body><div><main>...</main></div></body>❌ 嵌套了,AT 不识别 -
<body><main>...</main><main>...</main></body>❌ 重复,违反语义唯一性 - 组件库默认渲染一个
<main>,但页面有多个“主区块”?得合并或改用<section aria-labelledby="xxx"> - SSR 框架(如 Next.js)生成的 HTML 里
<main>被 wrapper div 包了一层?需检查 _document.tsx 或自定义 Document
标题层级断裂会让屏幕阅读器用户“掉层”
用户按 H 键逐级跳转时,<h2> 后直接 <h4> 会被当作章节结束,后面内容彻底丢失——这不是体验问题,是结构塌方。
真实原因常被忽略:
- CSS 用了
display: none隐藏某个<h3>,但 DOM 还在 → 改用aria-hidden="true"+ 视觉隐藏类 - 卡片组件默认用
<h2>,嵌套在<h2>主区域下 → 统一降级为<h3>或用<section>重置层级 - JS 动态插入标题没校验层级 → 插入前检查上一个标题级别,自动补
<h3>或调整 - 每个
<section>应以<h2>或更高级别开头,否则 AT 不认为它是独立语义区块
nav 和 role=button 这类标签不能滥用
<nav> 不是“所有链接容器”,而是有明确导航意图的区块。AT 用户按 N 键跳转时,会一口气进入所有 <nav>,里面塞一堆“相关阅读”或登录链接,等于直接劝退。
实操边界:
- 主导航、面包屑、页内目录 ✅ 可用
<nav>,且要加aria-label区分,比如<nav aria-label="主导航"> - 文章末尾“猜你喜欢”、侧边栏“热门标签” ❌ 改用
<div role="region" aria-labelledby="xxx"> - 用
<div onclick="...">模拟按钮?❌ 必须用<button>,否则键盘无法聚焦、无原生 focus 状态、AT 不播报“按钮”角色 -
<div role="button">是兜底方案,仅用于无法改标签的遗留系统,且必须手动加tabindex="0"和onkeydown回车/空格支持
真正难的不是写对单个标签,而是让整个 DOM 树在无样式、无 JS、仅靠键盘和屏幕阅读器时,依然能线性读出逻辑流。很多问题在视觉上完全看不出来,但对 AT 用户就是不可用。



















