development
什么是 ARIA?角色、地标与实时区域
ARIA 让开发者能够把动态网页内容变得对屏幕阅读器友好。了解角色、地标和实时区域如何工作——以及什么时候不该用它们。
ARIA 是什么——以及不是什么
ARIA 是 Accessible Rich Internet Applications(无障碍富互联网应用)的缩写。它是一组由 W3C 的网络无障碍倡议(WAI)定义的属性,让开发者能够把用户界面元素的含义和状态传达给屏幕阅读器之类的辅助技术。
关于 ARIA 最重要的一条规则,同时也是最常被忽视的一条:当原生 HTML 能够胜任时,就不要用 ARIA。<button> 元素本身就会宣告自己是按钮,并响应键盘事件。<nav> 元素本身就会把导航地标传达给屏幕阅读器。给一个 <div> 加上 role="button" 再用脚本实现它的行为,维护起来更难、更容易出错,而且通常在无障碍上的效果还不如一开始就用对元素。
ARIA 做不到的事:
- 让内容变得可见或可交互——它只改变辅助技术宣读的内容
- 修复失效的键盘访问——你仍然需要
tabindex和事件监听器 - 替代结构良好的语义化 HTML
ARIA 真正能帮上忙的场合,是原生 HTML 根本没有对应元素的时候:日期选择器、实时通知横幅、自定义组合框、树状视图。在这些情形下,ARIA 让你能够表达 HTML 无法表达的语义。
ARIA 角色
角色(role)告诉辅助技术它面对的是哪一类元素。每个可交互元素都有一个由其 HTML 标签推导出的隐式角色。<a> 元素具有 link 角色,<input type="checkbox"> 元素具有 checkbox 角色,<h2> 元素具有 heading 角色。
当你构建一个没有 HTML 对应物的自定义元素时,就要为它显式指定一个角色:
<!-- 用 div 构建的自定义开关 -->
<div
role="switch"
aria-checked="false"
tabindex="0"
>
深色模式
</div>
现在屏幕阅读器会把它宣告为一个开关控件,并读出它的选中状态。如果没有这个角色,它只会被当作纯文本宣读,用户根本不会知道它是可交互的。
常见角色及其适用场景
**控件角色(Widget roles)**描述可交互控件:
| 角色 | 何时使用 |
|---|---|
button | 一个没有更合适 HTML 标签的自定义可点击元素 |
checkbox | 自定义的多选开关 |
combobox | 文本输入框与列表框下拉菜单的组合 |
dialog | 模态浮层(仅在未使用原生 <dialog> 元素时) |
listbox | 自定义下拉列表 |
slider | 自定义的区间控件 |
switch | 开/关切换开关 |
tab、tablist、tabpanel | 选项卡式界面 |
tooltip | 悬停或获得焦点时显示的简短说明 |
**文档结构角色(Document structure roles)**描述非交互内容:
article——一段自成一体的内容figure——带图注的图像list、listitem——当需要在非列表元素上表达列表语义时presentation/none——移除元素的隐式角色(应少用、慎用)
一个致命错误:加了角色却没有行为
每个角色都附带一份”契约”,屏幕阅读器用户和键盘用户都期待它被兑现。role="button" 必须同时响应 Enter 键和空格键。role="checkbox" 必须能用空格键切换。role="link" 必须能用 Enter 键跳转。
如果你只加了角色却没有实现相应的行为,就等于在主动误导使用辅助技术的用户。他们听到一个控件被宣告出来,于是用预期的快捷键去操作它,结果什么也没发生。这比完全不用 ARIA 还糟糕。
ARIA 地标
地标(landmark)是页面的导航骨架。它们让屏幕阅读器用户能够在主要区域之间跳转,而不必把中间的内容全部听一遍——相当于视力正常的用户一眼扫过页面版式。
HTML5 引入了一批语义化元素,它们直接对应各个地标角色。用了它们,地标就是免费附赠的:
| HTML 元素 | 地标角色 | 用途 |
|---|---|---|
<header> | banner | 全站页眉(仅当它是顶层页眉、而非位于 <article> 内部时) |
<nav> | navigation | 导航菜单 |
<main> | main | 页面主要内容 |
<aside> | complementary | 与主内容相关的次要内容 |
<footer> | contentinfo | 全站页脚 |
<form> | form | 表单(仅当它有无障碍名称时) |
<section> | region | 具名区块(仅当它通过 aria-label 或 aria-labelledby 拥有无障碍名称时) |
你不需要给 <main> 元素再加 role="main"——那是多余的。只有在你无法使用语义化 HTML 元素时才需要 ARIA 地标角色,例如在一个生成 <div class="sidebar"> 的遗留代码库中:
<div class="sidebar" role="complementary" aria-label="相关文章">
<!-- 侧边栏内容 -->
</div>
当同一类地标不止一个时,要为它们加标签
当一个页面上出现同一类地标的多个实例时——两个 <nav> 元素、两个带 role="region" 的 <section> 元素——每一个都必须拥有唯一的无障碍名称,这样用户才能区分它们:
<nav aria-label="主导航">...</nav>
<nav aria-label="页脚导航">...</nav>
没有标签时,屏幕阅读器会把两者都简单地宣读为”导航”。有了标签,用户听到的是”主导航,导航地标”和”页脚导航,导航地标”,就能从地标列表中选中正确的那一个。
ARIA 实时区域
实时区域(live region)是页面上内容会动态更新的一块区域,并且这些更新应当自动播报给屏幕阅读器用户,而不需要他们把焦点移过去。
核心属性是 aria-live,它接受三个值:
off——更新不会被播报(所有元素的默认值)polite——在用户完成当前任务之后播报更新assertive——立即打断屏幕阅读器正在朗读的内容来播报更新
<!-- 表单提交后填充的状态消息区域 -->
<div aria-live="polite" id="status-message"></div>
<script>
document.getElementById('status-message').textContent =
'你的消息已发送。';
</script>
当文本内容发生变化时,设置了 aria-live="polite" 的屏幕阅读器会等待朗读出现停顿,然后播报新内容。绝大多数动态更新都应使用 polite。只在关键失败场景下才保留 assertive——例如支付错误、会话超时警告——这类信息紧急到足以打断用户。
实时区域的角色简写
有两个角色把 aria-live 的语义打包进了单个属性:
role="status"——等价于aria-live="polite"。用于成功消息、加载状态和非紧急更新。role="alert"——等价于aria-live="assertive",并且隐含aria-atomic="true"。用于错误消息和关键失败。
<!-- 错误会立即被播报,打断当前朗读 -->
<div role="alert" id="payment-error"></div>
<!-- 状态更新会在当前朗读之后礼貌地播报 -->
<div role="status" id="cart-count">购物车中有 3 件商品</div>
实时区域的常见错误
在元素进入 DOM 之前就填入内容。 浏览器是在元素首次被解析时注册实时区域的。如果你在插入元素的同时设置它的文本内容,某些屏幕阅读器会完全错过这次播报。请始终把实时区域容器写在初始 HTML 中,之后再通过 JavaScript 更新它的内容。
什么都用 assertive。 assertive 实时区域会打断用户正在做的一切,包括其他播报。搜索结果计数随用户输入而更新,并不需要 role="alert"。滥用 assertive 会给屏幕阅读器用户带来极不友好的体验。
实时区域更新过于频繁。 如果一个进度指示器每 100 毫秒就更新一次实时区域,朗读队列会溢出,用户什么有用的信息都听不到。只在有意义的节点上更新实时文本——25%、50%、75%、完成——或者用定时器对更新做防抖处理。
忘了 aria-atomic。 默认情况下,实时区域内只有发生变化的那个文本节点会被播报。如果你希望整个区域被重新朗读(而不只是变化的片段),请加上 aria-atomic="true":
<div aria-live="polite" aria-atomic="true">
还剩 <span id="count">2</span> 件
</div>
如果没有 aria-atomic,当计数从 3 变成 2 时,屏幕阅读器可能只播报”2”。加上它之后,完整的短语”还剩 2 件”才会被读出来,而这几乎总是你想要的效果。
其他必备的 ARIA 属性
aria-label——在没有合适的可见文本时提供无障碍名称:
<button aria-label="关闭对话框">✕</button>
aria-labelledby——指向另一个元素,用它的文本作为无障碍名称。当标签文本已经显示在页面上时,它优于 aria-label:
<h2 id="billing-heading">账单地址</h2>
<form aria-labelledby="billing-heading">...</form>
aria-describedby——指向补充文本,用来在名称之外进一步描述某个元素:
<input type="password" aria-describedby="pwd-hint">
<p id="pwd-hint">至少 12 个字符,并且要包含一个符号。</p>
aria-expanded——表示一个可折叠元素(下拉菜单、手风琴、菜单)当前是展开还是收起。每当状态变化时,都要在 JavaScript 中更新它:
<button aria-expanded="false" aria-controls="nav-menu">菜单</button>
<ul id="nav-menu" hidden>...</ul>
aria-hidden="true"——把元素从无障碍树中移除。适用于装饰性图标、重复文本,或者那些对屏幕阅读器用户只会增加噪音的视觉元素:
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">5 星中的 4 星</span>
aria-disabled="true"——把控件标记为禁用,但不把它移出焦点顺序。当你希望用户能发现该控件存在、并理解它为何不可用,而不是让它被悄无声息地跳过时,这很有用:
<button aria-disabled="true">提交(请先填写所有字段)</button>
在实践中测试 ARIA
写 ARIA 属性并不难,把它们写对则需要测试。自动化扫描器能发现明显错误的写法——没有无障碍名称的 role="button"、指向不存在 ID 的 aria-labelledby、aria-live 取值无效的实时区域。它们无法告诉你被播报出来的文本在上下文中是否说得通,也无法告诉你一个复杂的自定义控件在仅用键盘导航时行为是否正确。
要做贴近真实的测试,请至少组合两套屏幕阅读器与浏览器的搭配:
- NVDA + Chrome(Windows)——免费、使用广泛,接近真实世界的屏幕阅读器用户分布
- VoiceOver + Safari(macOS 或 iOS)——系统内置,是移动端无障碍测试的必备
- JAWS + Chrome 或 Edge(Windows)——付费,但在企业环境中是使用最广泛的屏幕阅读器
仅用键盘导航你的可交互组件。仔细听:当你打开菜单、提交一个带校验错误的表单,或触发一次实时区域更新时,究竟播报了什么。如果播报含糊或具有误导性,那么这个 ARIA 实现就是错的——不管自动化工具报出什么结果。
一条涵盖一切的规则
ARIA 规范包含五条编写规则。第一条最为重要:
如果你可以使用一个已经内置了所需语义和行为的原生 HTML 元素或属性,那就使用它,而不要改造某个元素、再添加 ARIA 角色、状态或属性来让它变得无障碍。
用语义化 HTML 来构建。只有当 HTML 力有不逮时才动用 ARIA。用真实的屏幕阅读器测试。这几乎覆盖了你会遇到的每一种情形。
如果想切实检查 ARIA 属性在你整个网站上的实现情况,我们的人工无障碍审计包含由每天使用辅助技术的人员进行的屏幕阅读器测试,他们能发现自动化工具遗漏的细微 ARIA 问题。