屏幕阅读器支持
通过提供语义 HTML 和 ARIA 确保应用用屏幕阅读器(NVDA、JAWS、VoiceOver、TalkBack)可用。
为什么
屏幕阅读器是盲或低视力用户访问应用的方式——它朗读页(或输出到盲文显示)并按语义结构(标题、区域、表单、链接)导航。无语义 HTML 和 ARIA,屏幕阅读器见一堆无意义 div: 不能宣按钮为按钮、表单错误为错误、购物车更新为变更。用户迷。语义 HTML(button、nav、main、h1-h6、label、fieldset)免费提供意义,ARIA (aria-label、aria-live、aria-expanded、role)填自定义 widget 缺口。支持屏幕阅读器非小众——是仅美国 700 万盲用户访问 web 的方式,是 ADA 下法律要求。
何时使用
每个面向用户功能。自定义 widget 开发时用屏幕阅读器测,发布前全过。动态内容或自定义交互变时重测。
如何做
1. 先用语义 HTML——是基础
- <button>、<a>、<input> 无任何 ARIA 正确宣读
- <nav>、<main>、<aside>、<footer> 创可导航区域
- <h1> 到 <h6> 创用户可按导航的标题大纲(H 键)
- <label> 关联 <input> 使字段带标签宣读
- <fieldset> + <legend> 分组相关输入(单选按钮、复选框)
2. 仅语义 HTML 不足时加 ARIA
- aria-label: 无可见文本时提供标签(仅图标按钮)
- aria-labelledby: 指向标此元素的可见元素
- aria-describedby: 指向帮助文本或错误消息
- aria-live="polite": 宣动态变更(购物车更新、toast、搜索结果)
- aria-expanded: 指示下拉或手风琴开或关
- role: 仅必要时覆盖隐式 role(罕见——优先语义 HTML)
3. 用实际屏幕阅读器测
- NVDA(免费)+ Windows 上 Chrome 或 Firefox——最常见组合
- Mac 上 VoiceOver + Safari(Cmd+F5 切换)
- Android 上 TalkBack(设置 → 无障碍)
- 导航: H(标题)、F(表单)、K(链接)、R(区域)、Tab(交互)
4. 验证宣读质量
- 每交互元素宣其 role 和标签吗?("提交,按钮")
- 表单错误宣读吗?(aria-describedby 指错误或 aria-live)
- 动态变更宣读吗?(购物车 aria-live="polite"、错误 "assertive")
- 标题结构合理吗?(一个 h1、然后 h2、无跳级)
5. 处理常见模式
- 仅图标按钮: <button aria-label="关菜单"><XIcon /></button>
- 加载状态: <div role="status" aria-live="polite">加载中...</div>
- 错误消息: <div role="alert">{error}</div>(隐式 aria-live="assertive")
- 标签 widget: 用 role="tab"、role="tablist"、role="tabpanel"、aria-selected、aria-controls示例
// 语义 HTML: 带标签和错误的表单
<form>
<fieldset>
<legend>送货地址</legend>
<div>
<label htmlFor="street">街道地址</label>
<input id="street" type="text" aria-describedby="street-error" required />
<div id="street-error" role="alert">{errors.street}</div>
</div>
</fieldset>
<button type="submit">下单</button>
</form>
// 屏幕阅读器: "街道地址,编辑,必填。[错误] 街道必填。"
// 仅图标按钮: 提供无障碍名
<button aria-label="关对话框" onClick={close}>
<XIcon aria-hidden="true" />
</button>
// 屏幕阅读器: "关对话框,按钮"
// 动态内容: 宣购物车更新
function AddToCartButton() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>加购物车</button>
<div role="status" aria-live="polite">
购物车中 {count} 件
</div>
</div>
);
}
// 屏幕阅读器: "加购物车,按钮。购物车中 0 件。"
// 点击后: "购物车中 1 件。"(自动宣读)
// 自定义标签 widget: 用 ARIA role 和属性
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">详情</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-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>
// NVDA 测: 按 H 按标题导航、F 按表单、Tab 过交互
// 验证: 每元素宣其 role、标签和状态常见问题
- ⚠给 <div onClick> 加 role="button" 而非用 <button>——div 现宣为按钮但仍不可键盘聚焦或激活,屏幕阅读器用户知是按钮但不能用。
- ⚠用重复可见文本的 aria-label——屏幕阅读器宣"关,关"因 aria-label 覆盖并重复文本,使输出比无 ARIA 更坏。
- ⚠动态内容忘 aria-live——购物车数视觉更新但屏幕阅读器用户永不被告诉,他们以为按钮没工作。
- ⚠为样式用标题级(小字体用 h2、粗体用 h3)而非结构——标题大纲无意义,屏幕阅读器用户不能按导航。
最佳实践
- ✓以语义 HTML(button、nav、main、h1-h6、label、fieldset)为基础——免费提供 role、标签和键盘支持,ARIA 仅自定义 widget 需。
- ✓至少用两个屏幕阅读器(NVDA + VoiceOver)真实浏览器测——不同屏幕阅读器不同解 ARIA,测两者抓不一致。
- ✓非紧急更新(购物车、搜索结果)用 aria-live="polite"、错误用 role="alert"(隐式 assertive)——礼貌级决定宣读是否打断用户。
- ✓维护合理标题结构(一个 h1、然后 h2、无跳级)——标题大纲是屏幕阅读器用户主要导航方法,坏结构使页不可导航。