无障碍 React
每个 React 应用都呈现为浏览器一直以来交付的相同 HTML,每个辅助工具都从你的组件生成的 DOM 中工作。React 中的无障碍设计主要是关于那个 DOM 的一系列小选择:你呈现什么元素、它如何获得名称,以及当屏幕变化而某人看不见时你做什么。
先说一个 JSX 细节。React 把 class 改成 className,把 for 改成 htmlFor,但 ARIA 属性保留连字符:aria-live、aria-label,还有普通的 role。
语义化元素优先
一个 <button> 自带一堆行为。它在 Tab 顺序中,所以键盘能到达它。它在 Enter 和 Space 上触发点击处理器。屏幕阅读器把它读作一个按钮,读出它的文本作为名称,这也是语音控制软件的目标名称。浏览器处理禁用状态、焦点环和激活样式。
// 浏览器给你焦点、键盘激活和"按钮"公告
<button className="die" onClick={hold}>{value}</button>一个带 onClick 处理器的 <div> 只能从那个列表中得到一项:点击。Tab 跳过它,Enter 和 Space 什么都不做,屏幕阅读器把它读作一段文本,没有暗示有任何交互会发生什么。
通常的补丁是加上 role="button" 和 tabIndex={0},这把元素放进 Tab 顺序并改变公告内容。但行为仍然缺失。你还要添加一个 onKeyDown 处理器,检查 Enter 和 Space,在 Space 上调用 preventDefault() 以防页面停止滚动,然后保持手写的禁用状态与样式同步。这是相当多的代码,只是为了重建浏览器已经提供的东西。直接用真实的 <button> 是更短的路,而且当浏览器改变时它保持正确。
同样的逻辑贯穿其余标记:<a href> 用于导航,<nav> 和 <main> 作为屏幕阅读器可以在其间跳转的地标,有序的标题作为人们导航的大纲。React 代码库中大部分无障碍工作是选择已经做这个工作的元素。
公告什么变化了
单页应用原地更新。没有页面加载告诉屏幕阅读器发生了什么,所以一个在屏幕中间呈现的变化可能完全无声。一个活跃区域移交那个信息:一个屏幕阅读器观察的容器,每当其内容改变时就公告。下面的 sr-only 类在视觉上隐藏它,使用本章后面介绍的 CSS 模式。
<div aria-live="polite" className="sr-only">
{isGameWon && <p>你赢了!按"新游戏"重新开始。</p>}
</div>包装器每次都呈现,最初是空的,当 isGameWon 翻转时 React 把一个段落换进去。那个顺序是人们常弄错的部分。携带 aria-live 的元素必须在内容到达前就在 DOM 中,因为屏幕阅读器在遇到活跃区域时注册它们,然后监视变化。在单一渲染中挂载区域及其文本,许多屏幕阅读器什么都不公告:整个东西看起来像普通的新内容。在树中保持一个空区域成本为零,使公告可靠。
aria-live="polite" 把公告放进队列。屏幕阅读器完成它当前在读的任何内容,然后在下一个自然停顿时交付你的消息,这可能落在视觉变化后一拍。那个延迟是刻意的,礼貌对几乎所有东西都是对的设置。
键盘交互
Tab 向前移动到可聚焦元素,Shift+Tab 向后移动,Enter 激活链接和按钮,Space 激活按钮并切换复选框。
Tab 顺序跟随 DOM 顺序,所以你的 JSX 呈现的序列就是人们移动的序列。用 CSS 在视觉上重新排序会留下一个在屏幕周围跳跃的 Tab 顺序,正 tabIndex 值出于目的也造成同样的混淆。tabIndex={-1} 是有用的那个:它使一个元素可从 JavaScript 聚焦,同时把它排除在 Tab 序列之外,这是对话框标题这样的焦点目标需要的。
再两个规则。保持焦点可见:除非你自己的 :focus-visible 样式替换它,否则避免 outline: none。保持可用的出口:一个故意在内部保持焦点的模态需要 Escape 来关闭,需要把焦点交回其触发器。
有意移动焦点
当 UI 改变形状时,焦点可能最终无处可去。某人激活一个按钮,按钮被移除或替换,焦点回落到 <body>。下一个 Tab 从页面顶部开始,读者已经失去了他们的位置。
解决方案是把焦点移到某个合理的地方,这是 ref 的合法用途之一:
function NewGameButton({ isGameWon, onNewGame }) {
const buttonRef = useRef(null)
useEffect(() => {
if (isGameWon) {
buttonRef.current.focus()
}
}, [isGameWon])
return <button ref={buttonRef} onClick={onNewGame}>新游戏</button>
}effect 在 React 把那个节点提交到屏幕后运行,所以元素在那里接受焦点。在 isGameWon 上守卫防止它在每次呈现时窃取焦点。
同样的模式覆盖其他常见时刻:一个对话框在打开时获取焦点,在关闭时交回给触发器,验证失败把焦点发送到第一个无效字段,删除一行把焦点移到替换它的行。下面的规则只有一行:如果你的代码移除了有焦点的东西,你的代码决定焦点接下来去哪里。
视觉隐藏文本
许多状态从布局显而易见,屏幕阅读器无声:一个字段旁的绿色复选标记、一个看起来被按下的骰子、一个从它坐的地方清晰读出的数字。视觉隐藏文本为任何听页面的人拼出来。
约定是一个叫 sr-only 的类。它对 React 或浏览器没有意义:它是一个普通的类名,这些 CSS 规则做的是工作。
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}该元素保留在无障碍树中,同时不占用视觉空间。display: none 和 visibility: hidden 会把它也从那棵树中移除,向所有人隐藏它。
仅图标按钮是日常情况。要么给它一个 aria-label,要么把真实文本放在里面并在视觉上隐藏:
<button onClick={onClose}>
<XIcon aria-hidden="true" />
<span className="sr-only">关闭</span>
</button>aria-hidden="true" 把装饰性 SVG 排除在公告外,隐藏的 span 提供名称。关于 aria-label 有个警告:它在交互元素和任何携带显式 role 的东西上设置无障碍名称,浏览器经常在普通 <div> 或没有 role 的 <span> 上忽略它。把它限制在按钮、链接、输入和标记地标上。
仅读源代码对这些东西中的任何一个都只能带你走这么远。用 Cmd+F5 打开 VoiceOver 并听你自己的应用,在浏览器中运行 axe DevTools 来自动捕捉缺失的标签和未命名的控件。
button,每个输入旁有真实 label。这些元素自带键盘支持和屏幕阅读器能读的名称,全部免费。 当屏幕上有东西改变,一个听页面的人会不然错过,把一个短句放在一个带 aria-live="polite" 的 div 中,从一开始就把那个 div 保持在页面上以便注意到变化。
接下来:超越基础,一个基础之后来什么的地图。

