前端无障碍实战:用语义化与 ARIA 打造人人可用的网站

前端无障碍(Accessibility,常缩写为 a11y)不是锦上添花,而是网站能不能被所有人使用的底线。据 WHO 统计,全球有超过 10 亿人存在某种形式的障碍;当你的按钮没有可访问名称、图片没有 alt 文本、弹窗无法用键盘关闭时,这群用户会被直接挡在门外。本文从语义化 HTML、ARIA、键盘可达、对比度到自动化检测,给出一套可立即落地的前端无障碍实战清单。

一、先搞清楚:前端无障碍到底在解决什么

无障碍(a11y)指的是让视觉、听觉、运动、认知有障碍的用户,也能平等地感知、理解并操作网页。它通常遵循 W3C 的 WCAG(Web Content Accessibility Guidelines) 标准,分为 perceivable(可感知)、operable(可操作)、understandable(可理解)、robust(健壮)四个原则,简称 POUR。

对技术团队来说,做前端无障碍有三重收益:一是覆盖更大的用户群;二是可访问的语义化结构往往也利于 SEO 与屏幕阅读器之外的爬虫;三是很多国家(如欧盟 EN 301 549、美国 ADA)已将网站无障碍纳入合规要求,遗漏可能带来法律风险。

二、语义化 HTML:成本最低的第一道防线

很多无障碍问题,根本不需要 ARIA,只要用对原生标签就能解决。屏幕阅读器依赖 DOM 的语义角色来播报,而 <div> 和 <span> 默认是“无角色”的,滥用它们构建 UI 会丢掉大量内置可访问性。

2.1 用对标签,而不是用 div 模拟

<!-- 反面教材:用 div 拼按钮,屏幕阅读器读不出“按钮” -->
<div class="btn" onclick="submit()">提交</div>

<!-- 正确:原生 button 自带角色、键盘可达、焦点可见 -->
<button type="submit">提交</button>

<!-- 链接用 a,导航用 nav,正文分区用 main/section/article -->
<nav aria-label="主导航">
  <ul><li><a href="/">首页</a></li></ul>
</nav>
<main>
  <article>...正文...</article>
</main>

2.2 用 landmark 帮用户快速跳转

header / nav / main / aside / footer 这些 landmark(地标)区域,能让屏幕阅读器用户用快捷键在不同区块间跳转,而不是从头读到底。一个页面应有且仅有一个 main,多个 nav 要用 aria-label 区分。

三、ARIA:当原生语义不够时再出场

ARIA(Accessible Rich Internet Applications)通过 role 和 aria-* 属性补充语义,但它有一条黄金法则:能用原生 HTML 就不要加 ARIA。错误的 ARIA 比没有 ARIA 更糟,因为它会向辅助技术撒谎。

3.1 给自定义组件补上角色与状态

<!-- 自定义 Tab 切换:用 role 声明结构,用 aria-selected 暴露状态 -->
<div role="tablist">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">概览</button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2">详情</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>

3.2 用 aria-live 播报动态内容

表单校验错误、Toast 提示、加载状态这类“动态出现”的内容,屏幕阅读器默认不会主动播报。给容器加 aria-live="polite",辅助技术会在内容变化时朗读出来,不打断当前操作。

<div aria-live="polite" class="toast">
  <!-- 当这里文本变化(如“已保存”“验证码错误”),会被朗读 -->
</div>

四、键盘可达与焦点管理

很多用户完全不用鼠标。可操作元素必须能用 Tab 到达、用 Enter/Space 触发,且焦点顺序要符合视觉顺序。最容易被忽视的是模态框:打开时焦点应移入弹窗,关闭后回到触发元素,且 Tab 不能“逃出”弹窗。

4.1 实现一个焦点陷阱(focus trap)

function trapFocus(container) {
  const focusable = container.querySelectorAll(
    'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0], last = focusable[focusable.length - 1];
  container.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault(); last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault(); first.focus();
    }
  });
}

4.2 跳过链接(skip link)

在 body 开头放一个“跳到主内容”的链接,默认隐藏、获得焦点时显示,键盘用户就能一键绕过冗长导航直达正文,这是 WCAG 2.4.1 的明确要求。

五、可访问的表单:每个控件都要有名字

屏幕阅读器读表单时,靠的是“控件 + 关联标签”的组合。没有 <label> 的输入框,用户听到的只是“编辑框”,完全不知道要填什么。错误提示要用 aria-describedby 关联到输入框。

<label for="email">邮箱</label>
<input id="email" type="email" aria-required="true"
       aria-describedby="email-err">
<p id="email-err" role="alert">邮箱格式不正确</p>

六、颜色对比度:别让低视力用户看不见

WCAG 对正文文本要求对比度至少 4.5:1(大号文本 3:1)。浅灰字配白底、品牌色做正文是常见雷区。可以用浏览器开发者工具或对比度检查器核对。

等级对比度要求适用场景
AA(普通文本)≥ 4.5:1绝大多数正文、按钮文字
AA(大文本 ≥24px)≥ 3:1标题、大字标语
AAA(普通文本)≥ 7:1无障碍高标准站点
UI 组件 / 图形≥ 3:1输入框边框、图标按钮

另外,不要只用颜色传达信息。表单错误除了标红,还应加图标或文字“错误”,否则色盲用户无法区分。

七、图片与多媒体:给非视觉用户留通道

装饰性图片用 alt="" 让屏幕阅读器跳过;承载信息的图片必须写清楚它“表达什么”。纯文字图片(如带标题的 banner)尤其要避免,因为读屏软件读不到图里的字。

<!-- 装饰图:空 alt,被忽略 -->
<img src="/deco.png" alt="">

<!-- 信息图:描述含义而非“一张图” -->
<img src="/sales.png" alt="2026 年 Q3 销售额环比增长 23%,主要来自华东大区">

八、用工具把无障碍做成可自动检查的项

人工审计成本高,先让工具兜底 30% 的硬错误。Lighthouse 的 Accessibility 审计、axe DevTools 浏览器插件都能在开发期直接标红问题;在 CI 里集成 axe-core 或 eslint-plugin-jsx-a11y,可以把无障碍问题挡在合并之前。顺带一提,Lighthouse 同样覆盖前端性能优化的审计项,一次跑测两件事。在 VS Code 中装一个 axe 插件,写代码时就能实时看到警告。

工具类型擅长
Lighthouse浏览器/CLI一键出综合无障碍评分与改进项
axe-core / axe DevTools库 / 插件细粒度规则、CI 可集成
eslint-plugin-jsx-a11ylint 规则React/Vue 写码期即报错
WAVE / 读屏实测在线 / 人工终态验收、真实体验验证

九、在 Vue / React 框架里落地

框架本身不阻碍无障碍,但 SPA 路由切换、动态内容会让焦点“丢失”。以 Vue 3 为例,路由切换后手动把焦点移到新页面标题,能保证键盘和读屏用户不会停留在旧位置:

// Vue Router 切换后,把焦点交给主区域
router.afterEach(() => {
  nextTick(() => {
    const main = document.querySelector('main');
    if (main) {
      main.setAttribute('tabindex', '-1');
      main.focus();
    }
  });
});

在 GitHub Actions 的构建步骤里跑一次 axe-core,能让每次 PR 都自动过一道无障碍门禁,避免后期返工。

十、上线前无障碍检查清单

检查项方法对应原则
仅用键盘能否走完主流程拔掉鼠标实操一遍Operable
所有图片有合适 altaxe / 人工核对Perceivable
对比度达标对比度检查器Perceivable
动态内容有 aria-live读屏实测Robust
标题层级无跳跃检查 h1→h2→h3Understandable
CI 跑了 a11y 测试流水线绿Robust

小结

前端无障碍不是一次性改造,而是一种贯穿需求、开发、测试的工程习惯。记住优先级:先语义化、再 ARIA、最后用工具兜底。从今天起,给每个按钮用 <button>、给每张图写 alt、给表单加 label,你的网站就会对更多人真正“可用”。

上一篇 SonarQube 实战:用静态代码分析把 Bug 挡在合并之前
下一篇 Greenplum 实战:用 MPP 架构打造 PB 级数据仓库
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]