引言
可访问性(a11y)与国际化(i18n)经常被当作项目收尾的"锦上添花",但它们在工程上其实是同一件事的两种延伸:让产品服务于更广泛的用户。本文从 WCAG 2.2 四大原则出发,讲解 ARIA 的正确用法、键盘导航与焦点管理、屏幕阅读器兼容,以及基于 i18next/react-intl 的国际化工程方案,包括复数规则与 RTL 布局的落地细节。文中涉及 WCAG 2.2 成功标准、ARIA 规范、axe-core、i18next、react-intl(FormatJS)、Intl.PluralRules 等均为真实标准与真实库。
一、WCAG 2.2 四大原则:感知、操作、理解、健壮
1.1 POUR 原则框架
WCAG(Web Content Accessibility Guidelines)2.2 把无障碍要求归纳为四大原则(POUR):
| 原则 | 英文 | 含义 | 典型失败 |
|---|---|---|---|
| 感知 | Perceivable | 信息可被所有感官接收 | 图片无 alt、纯颜色传达状态 |
| 操作 | Operable | 交互可被键盘/辅助技术操作 | 焦点不可见、陷阱键盘 |
| 理解 | Understandable | 内容与操作可被理解 | 语言混乱、导航不一致 |
| 健壮 | Robust | 可被各种 UA/AT 可靠解析 | 语义错误、无效 ARIA |
1.2 关键成功标准速查
WCAG 2.2 中与前端工程师最相关的高频标准:
| 标准 | 级别 | 要求 | 落地 |
|---|---|---|---|
| 1.4.3 对比度 | AA | 正文对比度 ≥ 4.5:1 | 色彩 token 校验 |
| 2.4.7 焦点可见 | AA | 键盘焦点清晰可见 | focus-visible 样式 |
| 2.4.11 焦点不被遮挡 | AA(新增) | 固定定位不遮挡焦点元素 | scroll-margin 处理 |
| 3.2.6 一致帮助 | A(新增) | 帮助机制位置一致 | 帮助入口统一 |
| 4.1.2 名称/角色/值 | A | 控件有正确的语义 | ARIA + 原生语义 |
1.3 从"修漏洞"到"设计即无障碍"
无障碍的最佳实践不是在完成后修复,而是在设计阶段就纳入。一个务实的做法是建立a11y 需求检查单:每个新功能上线前,核对键盘可达、语义正确、焦点顺序合理、对比度达标。把这条检查单写进 PR 模板,比事后用工具扫描更根本。
无障碍不是一次性的「修复工程」,而是每个 PR 都要回答的常规问题:键盘能操作吗?读屏器能理解吗?焦点顺序合理吗?
二、ARIA 角色与属性:正确使用而非滥用
2.1 ARIA 的核心原则
ARIA(Accessible Rich Internet Applications)的作用是补充原生 HTML 无法表达的语义。它的第一原则是:能使用原生语义就绝不使用 ARIA——<button> 自带角色与键盘行为,比 <div role="button"> 更可靠。
<!-- 反模式:用 div 模拟按钮,需要手动补键盘行为 -->
<div class="btn" role="button" tabindex="0" onclick="submit()">提交</div>
<!-- 正确:直接用 button 元素 -->
<button type="button" onclick="submit()">提交</button>
2.2 常用 ARIA 属性速查
| 属性 | 作用 | 适用场景 |
|---|---|---|
| aria-label | 为无文本控件提供名称 | 图标按钮 |
| aria-labelledby | 引用其他元素作为名称 | 表单分组 |
| aria-describedby | 关联补充描述 | 表单错误提示 |
| aria-expanded | 指示展开/收起状态 | 折叠面板、下拉 |
| aria-controls | 关联控制的区域 | Tab、手风琴 |
| aria-live | 宣布动态内容变化 | 通知、错误汇总 |
| aria-current | 指示当前项 | 导航高亮 |
2.3 一个带完整语义的组件
以自定义下拉选择为例,展示 aria 属性如何协同工作:
<div class="select" role="combobox" aria-expanded="false" aria-haspopup="listbox">
<button id="select-trigger" aria-controls="select-listbox" aria-labelledby="select-label">
当前:全部
</button>
<ul id="select-listbox" role="listbox" aria-labelledby="select-label">
<li role="option" aria-selected="true">全部</li>
<li role="option" aria-selected="false">电子</li>
<li role="option" aria-selected="false">图书</li>
</ul>
</div>
ARIA 的陷阱是使用错误的值或相互矛盾的属性,这比不用更糟——辅助技术会把冲突信息读给用户。引入 axe 等自动检测工具能拦截大部分低级错误。
三、键盘导航与焦点管理
3.1 焦点管理的三件套
Web 无障碍的"操作"原则依赖键盘。焦点管理有三个核心问题:焦点是否可达、是否可见、是否在预期位置。
- tabindex:
0让元素可 Tab 聚焦,-1让元素可通过脚本聚焦但不可 Tab 到达(常用于焦点恢复)。 - focus-visible:仅键盘焦点时显示轮廓,鼠标点击不显示,避免样式闪烁。
/* 键盘焦点时才显示清晰轮廓 */
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* 下拉触发器的键盘交互 */
.dropdown-trigger:focus-visible {
box-shadow: 0 0 0 2px var(--color-focus-ring);
}
3.2 弹窗的焦点陷阱
弹窗(Dialog)是焦点管理最常出错的地方。正确行为:打开时焦点移入弹窗、Tab 循环限制在弹窗内、关闭后焦点恢复到触发按钮。React 中常配合 react-focus-lock 或手写守卫:
import { useEffect, useRef } from 'react';
function Dialog({ onClose, children }) {
const dialogRef = useRef(null);
useEffect(() => {
const previousFocus = document.activeElement;
dialogRef.current.focus(); // 焦点移入弹窗
const handleKey = (e) => {
if (e.key === 'Escape') onClose(); // Esc 关闭
};
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('keydown', handleKey);
previousFocus.focus(); // 恢复焦点
};
}, [onClose]);
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabIndex={-1}
>
<h2 id="dialog-title">确认操作</h2>
{children}
</div>
);
}
3.3 跳过链接与 Landmark
长页面键盘用户最痛的是每次进入都要 Tab 过整个导航。Skip Link(跳到主内容)是廉价的解法:
<a class="skip-link" href="#main-content">跳到主要内容</a>
<nav aria-label="主导航">…</nav>
<main id="main-content">…</main>
同时善用 HTML5 Landmark:<main>、<nav>、<aside>、<header>、<footer> 提供了语义地标,让屏幕阅读器用户可以直接跳转到区域。
四、屏幕阅读器:NVDA 与 VoiceOver 的兼容实践
4.1 两类主流屏幕阅读器
| 屏幕阅读器 | 平台 | 特点 |
|---|---|---|
| NVDA | Windows | 开源,配合 Firefox/Chrome |
| VoiceOver | macOS / iOS | 系统内置,macOS 用 Cmd+F5 开启 |
| TalkBack | Android | 安卓系统内置 |
前端工程师至少应掌握:用 NVDA + Firefox 与 VoiceOver + Safari 各走一遍核心流程。常见差异点:aria-label 的读法、aria-live 区域的触发时机、table 的读法在两种环境可能不同。
4.2 aria-live 的节奏控制
aria-live 用于宣布非焦点触发的动态变化(如"保存成功"提示)。节奏控制很关键:
polite:读屏器完成当前朗读后再播报,适合通知类。assertive:立即中断当前朗读,只用于紧急错误。
<!-- 表单提交后的成功提示 -->
<div role="status" aria-live="polite">
保存成功
</div>
<!-- 校验错误汇总 -->
<div role="alert" aria-live="assertive">
有 2 个字段填写错误
</div>
4.3 测试的核心误区
用屏幕阅读器测试时常见的误区是"每个控件都要有 aria-label"。事实是:只有原生语义无法表达时才需要 ARIA。一个 <label for="email">邮箱</label> 配 <input id="email"> 已经给出了控件名称,无需再叠加 aria-label。过度添加会让读屏器信息过载——“名称重复读三遍"就是典型症状。
<!-- 正确:原生 label 已提供名称 -->
<label for="email">邮箱</label>
<input id="email" type="email" autocomplete="email" />
<!-- 多余:label 已够用,不应再叠加 aria-label -->
<label for="email">邮箱</label>
<input id="email" type="email" aria-label="邮箱" />
五、色彩对比度与无障碍设计
5.1 对比度的硬性门槛
WCAG 要求普通文本对比度 ≥ 4.5:1,大字号(≥18pt 或 ≥14pt 加粗)≥ 3:1。非文本信息(图标、边框、焦点环)≥ 3:1。设计系统应建立对比度校验:任何新增色板都必须通过自动化比对。
// 用 real 工具校验对比度(概念示例,可接入设计 token)
// 对比度 = (L1 + 0.05) / (L2 + 0.05),L 为相对亮度
function contrastRatio(fg: string, bg: string): number {
// 简化示意:真实实现需计算 sRGB 相对亮度
const [r1, g1, b1] = hexToRgb(fg);
const [r2, g2, b2] = hexToRgb(bg);
const l1 = relativeLuminance(r1, g1, b1);
const l2 = relativeLuminance(r2, g2, b2);
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
5.2 不只颜色传达状态
“以红色表示错误"对色觉障碍用户(全球约 8% 男性)不可靠。错误提示应同时具备非颜色信号:图标、文本描述、role="alert"。
| 状态 | 仅颜色 | 无障碍版本 |
|---|---|---|
| 错误 | 红字 | 红字 + ⚠ 图标 + 错误文本 |
| 必填 | 红色星号 | 红色星号 + aria-required |
| 选中 | 高亮背景 | 高亮背景 + aria-selected |
| 连接状态 | 绿点 | 绿点 + 文本"已连接” |
5.3 深色模式与对比度
深色模式引入新的对比度风险:深底上的低饱和色容易低于 4.5:1。设计 token 在深色主题下应有独立的一套色板,并同样过校验,而不是简单取反色。
六、国际化 i18n:i18next 与 react-intl
6.1 两套主流方案的定位
- i18next:功能全面的通用 i18n 框架,支持嵌套 key、复数列、语言检测、资源加载,配合
react-i18next用于 React。 - react-intl(FormatJS):基于
Intl标准 API,消息用 ICU MessageFormat 语法,类型安全更好,配合@formatjs/cli可提取与管理消息。
// react-intl 的 Provider 与消息定义
import { FormattedMessage, useIntl } from 'react-intl';
const messages = {
'app.hello': '你好,{name}!',
'app.items': '{count, plural, one {# 个商品} other {# 个商品}}',
};
function Greeting() {
const intl = useIntl();
const text = intl.formatMessage({ id: 'app.hello' }, { name: 'Leeting' });
return <p>{text}</p>;
}
6.2 消息管理与命名规范
i18n 工程化的重点是消息管理:key 的命名规范、消息的提取、翻译文件的同步。推荐做法:
- key 使用点分命名:
page.checkout.submit,避免语义在翻译中丢失。 - 用工具提取硬编码字符串(react-intl 的 extract、i18next 的 scanner)。
- 翻译文件按语言组织,构建时打包对应语言。
// locales/zh-CN.json
{
"page.checkout.submit": "提交订单",
"page.checkout.confirm": "确认订单信息",
"page.cart.empty": "购物车是空的"
}
6.3 语言检测与资源加载
语言检测顺序建议:显式选择 > localStorage > navigator.language > 默认语言。注意不要把所有语言一次性打进 bundle,按需加载:
// i18next 按需加载语言资源
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n.use(LanguageDetector).init({
fallbackLng: 'zh-CN',
load: 'languageOnly',
resources: {
'zh-CN': { translation: zhCN },
'en': { translation: en },
},
});
七、复数与 RTL 布局
7.1 复数的陷阱
中文没有词形变化,但英语、俄语、阿拉伯语有复杂的复数规则。永远不要用字符串拼接处理复数。react-intl 的 ICU 语法与 Intl.PluralRules 能正确处理所有语言的复数类别:
import { FormattedMessage } from 'react-intl';
// 正确:交给 ICU 复数规则
<FormattedMessage
id="app.itemCount"
values={{ count: 5 }}
/>
{
"app.itemCount": "{count, plural, =0 {没有商品} one {# 个商品} other {# 个商品}}"
}
// Intl.PluralRules 是真实标准 API,可直接获得复数类别
const rule = new Intl.PluralRules('en-US');
rule.select(1); // 'one'
rule.select(5); // 'other'
7.2 RTL 布局:不只是镜像
阿拉伯语、希伯来语是 RTL(从右到左)语言。RTL 不是简单的镜像:阅读顺序、箭头方向、图标含义、文本对齐都要翻转。工程上:
- 用逻辑属性
margin-inline-start、padding-inline-end替代物理属性,让布局自动适配。 - 用
dir="rtl"设置文档方向,配合 CSSdirection。 - 图标类组件单独评审方向语义(前进/后退箭头)。
/* 使用逻辑属性,RTL 下自动翻转 */
.card {
margin-inline-start: 16px; /* LTR 为 left,RTL 为 right */
padding-inline-end: 8px;
text-align: start; /* 跟随文档方向 */
}
/* 多语言站点的 dir 切换 */
html[dir='rtl'] .arrow-next {
transform: scaleX(-1); /* 方向性图标翻转 */
}
7.3 布局弹性的验收清单
多语言站点的布局验收清单:文本换行不溢出、RTL 下无镜像错误、长单词(德语复合词)不撑破容器、日期/数字/货币按区域格式显示、Intl.DateTimeFormat 与 Intl.NumberFormat 使用正确:
// 日期与货币按区域格式化
const date = new Intl.DateTimeFormat('ar-EG').format(new Date(2026, 8, 27));
const price = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(1999.5);
八、自动化 a11y 测试:axe 与 lint 集成
8.1 axe-core:规则最全的自动检测
axe-core 由 Deque 开发,覆盖 WCAG 大部分可自动化检测的规则。在 Playwright 中集成:
// Playwright + axe 集成
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';
test('首页无严重可访问性违规', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations.filter((v) => v.impact === 'critical')).toEqual([]);
});
React Testing Library 也可以配合 jest-axe 做组件级检测:
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
it('组件无可访问性违规', async () => {
const { container } = render(<NavMenu />);
expect(await axe(container)).toHaveNoViolations();
});
8.2 lint 层的静态拦截
eslint-plugin-jsx-a11y 在编译前拦截常见的 a11y 反模式(img 无 alt、button 无可访问名、错误 ARIA 用法):
npm i -D eslint-plugin-jsx-a11y
// .eslintrc 或 eslint.config.js
export default [
{
plugins: { 'jsx-a11y': jsxA11y },
rules: {
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/aria-props': 'error',
},
},
];
8.3 自动检测的边界
必须诚实说明:自动化检测只能覆盖约 30-50% 的 WCAG 标准。焦点顺序是否合理、读屏器读起来是否流畅、语音导航是否可用——这些需要人工 + 屏幕阅读器的真实验证。最佳组合是:lint 拦截语法层错误 + axe 扫描 DOM 语义层问题 + 核心页面人工走查。
自动检测能兜住语法层与语义层的低级错误,但「读起来顺不顺」最终只有屏幕阅读器用户能回答——请真实用户走查,而不是只依赖工具。
九、a11y 与 i18n 的落地路线图
9.1 分层落地节奏
| 阶段 | 动作 | 工具 |
|---|---|---|
| 第一周 | 全站 lint 规则 + axe 扫描 | eslint-plugin-jsx-a11y、axe |
| 第二周 | 修复 critical 违规,规范语义组件 | 组件库 audit |
| 第三周 | 键盘导航与焦点管理走查 | 人工 + Playwright 焦点测试 |
| 第四周 | 屏幕阅读器核心流程走查 | NVDA / VoiceOver |
| 持续 | 新功能 a11y 检查单进 PR | 团队规范 |
9.2 i18n 改造的工程要点
存量项目接 i18n 的务实顺序:
- 先抽取界面文案到消息文件,建立语言资源体系。
- 接入语言检测与按需加载。
- 处理日期、数字、货币的区域化格式。
- 支持 RTL 语言(逻辑属性改造)。
- 建立翻译质量管理(CI 校验 key 完整性、缺失翻译拦截)。
# CI 中校验翻译 key 完整性
npx @formatjs/cli extract --format simple 'src/**/*.tsx' --out-file messages.json
node scripts/check-translations.js locales/
9.3 一句总结
可访问性不是给少数群体的额外工作,而是产品质量的底线指标:对色觉障碍、键盘用户、读屏用户的友好,往往同时提升了所有用户的可用性——语义化 HTML 对 SEO 友好,清晰的焦点管理对效率用户友好,逻辑属性对国际化友好。a11y 与 i18n 不是孤立模块,它们共同构成"产品面向世界"的基础设施。
结语
本文从 WCAG 2.2 的 POUR 原则出发,覆盖了 ARIA 的正确用法、键盘导航与焦点管理、屏幕阅读器兼容、色彩对比度、i18next/react-intl 国际化、复数与 RTL 布局,以及 axe 自动化测试。核心要点可以浓缩为三条:能用原生语义就用原生语义,ARIA 只在原生不足时补充;对比度与焦点是硬门槛,自动化工具只能兜住一部分;i18n 的本质是尊重每一种语言的形态与阅读方向。
最后给出一个实用的自检问题清单:这个功能能用键盘完成吗?屏幕阅读器能理解它的语义吗?切换到另一种语言或 RTL 后布局还正确吗?当团队把这三个问题写进验收标准,a11y 与 i18n 就从"加分项"变成了"不返工项”。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。