必须用 aria-live 与 aria-busy 组合并按生命周期控制:aria-live="polite" 用于非紧急更新,"assertive" 仅限紧急中断;二者须加在动态内容直接父容器上,配合 aria-busy="true"(请求开始时设、dom 稳定后立即清除)、aria-atomic="true" 及焦点管理,禁用 aria-hidden。

异步更新区域(比如 AJAX 加载的列表、搜索结果、仪表盘数据)若不加可访问性声明,屏幕阅读器大概率会静默跳过变化,或读出中间态的残缺内容——这不是“没提示”,而是“提示错了”。必须用 aria-live + aria-busy 组合,且要按生命周期控制。
用 aria-live 告诉屏幕阅读器“这里会变”
它不是开关,而是播报策略声明。关键不在“加没加”,而在“加在哪”和“值设对没”:
-
aria-live="polite"适用于非紧急更新,如分页加载新条目、购物车数量变更——它等用户当前操作停顿后再播报,避免打断 -
aria-live="assertive"仅用于必须立刻干预的场景,如表单提交失败弹出错误,但注意:它会强行中断当前语音,慎用 - 必须加在动态内容的**直接父容器**上,而不是整个页面或某个无关 wrapper;如果容器内嵌了多个子区域各自更新,应分别加,避免语义污染
- 别和
role="alert"混用:后者已隐含aria-live="assertive",重复写不会增强效果,反而增加冗余
用 aria-busy="true" 抑制不完整播报
它不是 loading 状态的视觉替代,而是给辅助技术的“请稍候”信号。常见误用是把它当 disabled 用,或忘了清理:
- 只在内容**开始异步请求时**设为
aria-busy="true",例如 fetch 发起后立即设置 - 必须在 DOM 更新完成、内容稳定后**立刻移除该属性**(或设为
false),否则屏幕阅读器会持续忽略后续变化 - 不能套在按钮或表单控件上——它只适用于**纯内容容器**(如
<div id="results">),对交互元素无效 <li>与 <code>aria-live配合时,aria-busy="true"会临时挂起 live 区域的播报,等 busy 移除后才触发最终播报,这是防止“读一半又重读”的关键机制 -
aria-atomic="true"强制播报整个容器内容,而非只变化部分——尤其适合替换整块 HTML 的场景(如整个表格重绘),避免读出“第3行更新为…”这种断裂信息 - 如果更新后需要用户立即操作(如新加载的按钮),得用
focus()主动聚焦到有意义的元素上;但别聚焦到容器本身——它没语义,屏幕阅读器只会读“div”,应聚焦到第一个可操作项或用tabindex="-1"+focus()做临时焦点锚点 - 禁用
aria-hidden="true"控制显隐:它会让整个区域从可访问树中消失,跟aria-busy的“暂不播报”逻辑冲突,属于典型误用
别漏掉 aria-atomic 和焦点管理
光有 live 和 busy 还不够,用户可能只听到片段,或根本不知道更新发生在哪:
最易被忽略的是生命周期同步:busy 属性的设置和清除必须严格对应请求的 start 和 resolve,任何 Promise race 或异常未捕获都会导致 busy 残留,让区域永久“失声”。这不是一次配置就能完事的事,得把它当成状态机的一部分来维护。











