wcag 2.1 aa 合规需从 html 根节点起步:必须显式声明 doctype 和 lang 属性,使用语义化标签替代 div+aria,严格规范 alt、label、aria 属性及表单可访问性闭环,并依赖 axe-core 运行时检测。

不重写 DOM 结构,只加 ARIA 或改 CSS,不可能达到 WCAG 2.1 AA 合规。真正的起点是 标签本身——它不是装饰,而是可访问性树的根节点。
DOCTYPE 和 lang 属性必须显式声明且不可省略
很多项目在 SSR 模板里漏掉顶层校验,结果整个页面从第一行就失效。Chrome DevTools 的 Elements 面板里,第一行必须是 ,第二行必须是 <code>(语言值需匹配实际内容)。
- 禁用
这类旧式声明,会触发怪异模式,无障碍树构建异常 - Next.js/Nuxt/Vite 项目要检查
index.html或app.html模板,而不是只看组件内渲染的片段 -
lang值不能是空字符串、auto或模糊值如zh;zh-CN和zh-TW是不同语种,不可混用
用原生语义标签代替 div + role
<nav></nav>、<main></main>、<article></article> 等不是“换名游戏”,它们自带隐式 role、焦点管理逻辑和屏幕阅读器内置行为。用 <div role="navigation"> 替代 <code><nav></nav>,等于手动模拟一套浏览器已内置的机制,漏一项就断裂。
-
<main></main>在整页中只能出现一次,且不能嵌套在<section></section>或<article></article>内部 -
<nav></nav>应包裹导航链接,若只是页眉 logo 区域,不要强行套<nav></nav> -
<section></section>不等于“侧边栏 CSS 类”;它代表有独立标题、可被单独引用的内容块,广告位更适合用<aside></aside>
img 的 alt 必须按功能分层写,不能省略或填无效值
alt 不是“补充说明”,而是图像在语义网中的文本锚点。缺失 alt 属性、写成 alt=""、或填 alt="图片",三者语义完全不同,后果也不同。
- 装饰性图片(分隔线、背景 icon)→ 必须写
alt="",不能删属性,也不能写alt="装饰图" - 信息图/按钮图标 → alt 要说明动作目标,比如
alt="查看购物车,当前有2件商品",不是alt="cart-icon.png" - 链接内唯一图片 → alt 不描述图像本身,而说明点击后行为;同一张图在不同链接中,alt 必须重写
- CMS/富文本编辑器上传时默认填文件名,这类值等于没写,需在模板层强制拦截或覆盖
表单控件必须形成可访问性闭环
一个可被准确识别、键盘操作流畅、状态反馈明确的表单,三者缺一不可。只加 <label></label> 或只写 aria-describedby,都是半残废。
-
<input>必须有唯一id,对应<label for="xxx"></label>,大小写、空格、拼写必须完全一致 - 错误状态需同时设
aria-invalid="true"和aria-describedby="error-id",指向实时错误文案容器 - 禁用态必须用原生
disabled属性,不能只靠opacity: 0.5+pointer-events: none,否则键盘仍可聚焦 -
<button></button>内含<svg></svg>时,<svg></svg>必须设aria-hidden="true",否则屏幕阅读器会重复读图标描述
最容易被忽略的是:所有这些规则都依赖 DOM 渲染后的最终结构。静态扫描工具(如 HTMLHint)能抓到缺失 alt 或错写 lang,但发现不了 aria-hidden="true" 错包了焦点元素、或 role="button" 缺少键盘事件监听——这些必须靠 axe-core 在运行时检测。别让“看起来没问题”骗过自己。











