mcpField
A
aria-live-regions

aria-live-regions

github.com/mgifford设计与 UI57160 次安装6 周前更新
代码仓库

关于 aria-live-regions

aria-live-regions 是一项面向包含动态内容更新项目的无障碍技能,帮助开发者选择合适的状态播报方式。重点是控制播报时机、范围和频率,避免用实时区域替代可见内容或焦点管理。

主要功能

  1. 01区分 status、alert 和 log 用途
  2. 02说明 aria-live 与相关属性
  3. 03指导更新时机及内容范围
  4. 04提供搜索、表单和进度示例

适用场景

  1. 01为保存或搜索结果添加状态消息
  2. 02设计动态表单错误与进度反馈

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mgifford/accessibility-skills --skill aria-live-regions

安装后在 Claude Code 中输入/aria-live-regions确认已加载。

附带文件README.mdSYNC.md

详细介绍

ARIA 实时区域无障碍技能

规范来源:examples/ARIA_LIVE_REGIONS_BEST_PRACTICES.md(位于 mgifford/ACCESSIBILITY.md) 此技能源自该文件。如有疑问,以示例为准。

实现向辅助技术播报的任何动态内容更新时,请遵循这些规则。 仅当项目包含动态内容更新时,才加载此技能。


核心原则

实时区域让屏幕阅读器用户无需移动键盘焦点即可获取动态状态信息——它们适用于简洁的更新(结果数量、保存确认、进度信息)。**实时区域不是通用通知系统。**它们不能取代可见内容、原生 HTML、焦点管理或控件的程序化状态。播报时机不当或播报过多,可能会被错过、重复、打乱顺序或造成干扰。

优先使用可见状态消息——它们还能帮助放大内容的用户、有认知或学习障碍的用户,以及其他无法注意到页面其他位置变化的用户。应先使用原生 HTML 和程序化控件状态(例如 aria-expanded),再考虑使用实时区域。


严重程度级别(本技能)

级别含义
严重动态内容变化未向辅助技术传达任何信息;用户无法完成任务
较高对非紧急更新使用 assertive/role="alert",打断用户正在收听的内容;实时区域在需要播报的更新发生前尚未暴露
中等缺少状态更新;未播报成功或失败;播报内容过于冗长
轻微播报内容冗余;实时区域内容不必要地重复可见文本

WCAG 4.1.3 的实际要求

WCAG 4.1.3 要求以程序化方式确定现有状态消息,以便辅助技术无需移动焦点即可呈现这些消息。它并不要求为每次动态变化都创建新的状态消息——更新后的结果列表属于内容;类似“找到 18 个结果”的消息才是状态消息。对话框获得焦点属于上下文变化,而非状态消息。

应根据具体情况选择机制,而不是条件反射地默认使用某种机制:

情况首选机制
按钮展开/折叠内容保持焦点在按钮上;更新 aria-expanded
选项卡切换面板使用选项卡模式及其选中状态
模态对话框打开将焦点移入对话框
SPA 切换路由更新文档标题;将焦点移至逻辑上合适的标题
表单显示多条错误使用错误摘要并管理焦点;将每条错误与对应字段关联
保存/购物车/筛选/搜索完成显示简洁的 role="status" 消息
出现重要且有时效性的状况显示可见的 role="alert" 消息
某种状况需要立即作出决定使用警告对话框;将焦点移入其中
向按顺序排列的历史记录追加新条目语义适合时使用 role="log"

实时区域可以补充焦点管理——不应播报已经由焦点变化传达的相同信息。


重要:role="status"、role="alert" 和 role="log"

角色隐式 aria-live隐式 aria-atomic用途
role="status"politetrue无需立即采取行动的提示信息
role="alert"assertivetrue重要且通常有时效性的信息
role="log"polite—按顺序添加新信息的内容(聊天、活动日志)
<p id="cart-status" role="status"></p>
<p id="connection-alert" role="alert"></p>

除非经过验证的兼容性需求确有必要,否则不要在这些角色上冗余地添加显式的 aria-live/aria-atomic 值。 警报不会获得焦点,也不应要求用户回应才能将其关闭——如果用户必须停下来回应,请改用 ARIA APG alert dialog pattern,并实现完整的模态对话框键盘交互。不要将 role="alert" 用于常规验证、成功确认或每个字段错误。

<div role="log" aria-relevant="additions" aria-atomic="false">
  <p>Jordan uploaded quarterly-report.pdf.</p>
</div>

role="log" 需要一个可访问名称(通过标题 + aria-labelledby 提供),且每条记录都应能独立理解。

aria-live 的裸值(仅在没有合适的 live region 角色时使用):

值含义
off默认值——除非焦点位于该区域内,否则不会呈现更新
polite在下一个合适的时机呈现
assertive最高优先级——仅在必须打断时使用

assertive 表示优先级,但不保证任何特定的 AT 都会立即打断语音;实际行为取决于产品、设置和其他排队的输出。将 assertive/role="alert" 用于非紧急更新属于严重问题。


严重问题:aria-atomic、aria-relevant 和 aria-busy

aria-atomic(默认值为 false):当完整短语能提供必要上下文(“购物车中有 3 件商品”)时使用 true;当每次新增的内容都具有独立意义(新的聊天记录)时使用 false。status/alert 的默认值已经是 true。值为 true 时,也可能会呈现该区域的可访问名称/标签——请测试最终的完整短语,而不只是发生变化的文本节点。

<div role="status" aria-atomic="true" id="filter-status">
  Showing 24 of 156 results for "accessible forms"
</div>

aria-relevant——其默认值是 additions text,而不只是 additions。可选值:additions、removals、text、all。这些只是给 AT 的建议,并非传递保证。谨慎使用 removals/all——不要依赖对移除内容的播报来传达关键信息。

aria-busy——当某个区域正在接收一批相关更新时,将其设为 true;完成后重置为 false。AT 可能会延迟处理,直到该区域不再处于忙碌状态。

results.setAttribute('aria-busy', 'true');
renderResults(items);
results.setAttribute('aria-busy', 'false');

始终重置 aria-busy,包括发生错误后——异常后仍将其留为 true 是常见故障。不要将 aria-busy 作为唯一的加载指示;还要提供可见状态。


严重:更新时机 — 不要采用固定延迟的权宜做法

实时区域的行为取决于辅助技术 API 是否在相关内容发生变化之前观察到该区域——在同一次 DOM 操作中添加实时区域和已完成的消息,可能不会被识别。实用原则是:先公开一个稳定的实时区域,再在后续事件发生时更新其内容。 实时区域不必在页面初次加载时就存在——在组件或 SPA 中,只要实时区域在后续更新发生之前挂载,就可以先挂载,之后再更新。如果用户需要理解页面初次加载时的信息,应将其作为可见内容放入阅读顺序中,而不是依赖实时区域。

没有任何标准规定实时区域应使用可靠的 50ms、100ms 或其他固定延迟。 清空实时区域,并在超时后重新插入相同文本,可能导致内容漏报、重复、过时或顺序错乱——不要使用这种模式。 相反,应:

  • 保持一个稳定的播报节点
  • 在组件挂载后更新它
  • 让框架根据状态渲染消息
  • 快速连续发生多次变化时,只播报最终且有意义的结果
  • 屏蔽过时异步请求的响应
  • 当多个组件需要报告状态时,使用经过测试的应用级播报抽象

许多实现会对 role="alert" 进行特殊处理,因此动态渲染的警报内容通常会被播报——但这并不意味着 alert 等同于给任何新元素添加 aria-live="assertive",也不意味着页面初次加载时的警报能可靠播报。


严重:可见性和内容范围

仅在以下情况下使用视觉隐藏的实时区域:已有等效的可见信息,且确实需要为非视觉用户提供补充;或者需要用简短播报来描述复杂的可见更新。不要在可见消息和单独的隐藏实时区域中重复相同消息——这会导致重复播报。

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

如果实时区域预期会播报,切勿对其应用 hidden、display: none、visibility: hidden 或 aria-hidden="true"——如果空的实时区域必须保持可访问,以便在下次更新时使用,请避免 .status:empty { display: none; } 这类规则。

让实时区域保持精简——播报简短的状态,而不是整个结果容器、表单、表格或页面区域。避免嵌套实时区域,也不要在另一个实时区域中放置 role="alert"。交互式控件通常不应放在实时区域中;如果提示条包含操作,应保持足够长时间,以便用户找到并操作它,并为其设置单独的焦点策略。


实现模式

操作后的可见状态:

<button type="button" id="add-to-cart">Add notebook to cart</button>
<p id="cart-status" role="status"></p>
addButton.addEventListener('click', () => {
  addProduct('Notebook');
  cartStatus.textContent = 'Notebook added to cart. 3 items in cart.';
});

对过时请求进行处理的搜索结果:

let requestSequence = 0;
searchForm.addEventListener('submit', async (event) => {
  event.preventDefault();
  const sequence = ++requestSequence;
  results.setAttribute('aria-busy', 'true');
  resultStatus.textContent = `Searching for ${query}.`;
  try {
    const items = await searchProducts(query);
    if (sequence !== requestSequence) return; // stale response, ignore
    renderSearchResults(results, items);
    resultStatus.textContent = `${items.length} results found for ${query}.`;
  } catch (error) {
    if (sequence === requestSequence) {
      resultStatus.textContent = 'Search could not be completed. Try again.';
    }
  } finally {
    if (sequence === requestSequence) results.setAttribute('aria-busy', 'false');
  }
});

安全地转义或渲染不可信的结果数据。可以考虑延迟显示“正在搜索”消息,以免快速完成的操作接连播报加载和完成状态。

字符计数——仅在达到阈值时播报,而不是每次按键都播报:

const thresholds = new Set([100, 50, 20, 10, 0]);
let lastAnnounced = null;
summary.addEventListener('input', () => {
  const remaining = summary.maxLength - summary.value.length;
  remainingCount.textContent = remaining; // visible count updates continuously
  if (thresholds.has(remaining) && remaining !== lastAnnounced) {
    countStatus.textContent = `${remaining} characters remaining.`; // hidden status, threshold only
    lastAnnounced = remaining;
  }
});

表单错误: 显示可见的摘要并将焦点移至该摘要——这是焦点管理模式,摘要本身不需要实时区域。用户输入时,不要给每个字段错误都添加 role="alert";如果附近的 role="status" 不会重复播报焦点已经呈现的信息,则可用于单个异步字段检查。

进度: 使用原生 <progress>;播报有意义的里程碑或完成状态,而不是每次百分比变化。

需要用户响应的会话超时: 使用警报对话框(role="alertdialog"、aria-modal="true"、可访问名称和描述、初始焦点位于对话框内、键盘操作限制在对话框内,并在关闭时恢复焦点)——仅靠紧急播报并不能让定时交互变得易于访问。对于超时本身,请遵循 WCAG 2.2.1“可调整的时间限制”。


中等:管理多次更新

实时区域更新过快可能会让语音队列不堪重负,并掩盖用户需要的结果:

  • 优先播报用户明确操作的结果,再播报后台活动
  • 对由输入触发的筛选/搜索播报进行防抖
  • 将相关更新合并为一句有用的话
  • 取消或忽略过时的异步响应
  • 不要播报滚动、指针移动、动画帧或时钟刻度
  • 如果加载实际上会立即完成,就不要播报加载状态
  • 为持续的信息流(聊天、体育赛事、监控)提供暂停/静音控件
  • 避免针对同一事件同时发送 status 和 alert 消息

如果角色已经提供了相应上下文,就不要使用“通知”或“状态”等前缀。


框架与组件指南

运行时生成的 DOM 和无障碍树决定了无障碍体验,而不是框架。如果许多组件都需要播报器,请在应用根节点附近放置一个稳定的播报器;通过框架状态呈现播报内容(不要直接修改框架也在管理的节点);确保播报器已挂载,然后再通过后续状态变更进行更新;避免嵌套根节点/门户创建重复的活动播报器;将水合、路由转换和重新挂载视为可能重置区域的时机边界;合并状态更新,避免播报中间文本。

React 模式(先挂载,再通过 effect 更新;如果状态变更前区域已挂载,则无需固定延迟):

const [status, setStatus] = useState('');

useEffect(() => {
  // Runs after the region has mounted and rendered; no arbitrary timeout needed
}, [status]);

return (
  <>
    <div id="status-region" role="status" aria-atomic="true" className="visually-hidden">
      {status}
    </div>
  </>
);

框架工具可以协调优先级、去重和过时请求处理,但仍需要使用受支持的 AT 进行手动测试。


测试

**静态检查:**所选角色与消息用途相符;隐式角色属性与显式属性不冲突;区域在需要报告的更新发生前已暴露;更新期间区域未从无障碍树中隐藏;状态文本简短,且不会包裹大型且不断变化的容器;实时区域未嵌套;未挂载重复的播报器;成功和失败后,aria-busy 都会恢复为 false。

手动交互测试(针对每个重要流程):从远离更新的位置开始阅读内容;使用键盘发起操作;确认消息在需要时可见;确认礼貌提示不会造成不必要的打断;确认警报仅用于重要情况;触发快速更新,并验证只播报有用的结果;重复执行相同操作,并验证第二次结果也会播报;触发重叠请求,并确认不会播报过时结果;确认焦点仍符合逻辑,且不会因状态更新而移动。

自动化检查可以检测无效的 ARIA 值、相互冲突的显式/隐式属性、使用 aria-hidden 隐藏的实时区域、嵌套实时区域、重复 ID,以及测试完成后仍将 aria-busy="true" 留在组件上的情况。添加集成测试,断言用户操作后最终的 DOM 消息,包括错误和过时请求路径——DOM 断言通过并不能证明 AT 正确播报了消息。


常见故障

故障修正方法
一次操作中同时添加新的 live-region 元素及其消息先提供一个稳定区域,然后在后续事件发生后更新它
要求区域在页面初次加载时就存在要求在相关更新发生前提供该区域,包括在已挂载的组件中
在固定延迟(例如 50ms)后清除并重新插入文本移除通用超时;更新稳定状态,并测试重复操作的结果
对普通确认使用 assertive使用 role="status"
认为 assertive 能保证立即打断将其视为优先级;测试实际受支持的组合
为每个字段错误都添加 role="alert"使用错误摘要、字段关联和焦点管理
手风琴面板打开时将焦点移至内容让焦点保留在展开控件上;更新 aria-expanded
播报整个结果列表播报简洁的结果摘要
使用 display: none 隐藏空区域若之后的更新必须被播报,则保持稳定区域可见
播报每个字符、百分比或秒数播报有意义的进度节点和完成状态
发生异常后仍将 aria-busy="true" 保持不变在每条完成路径中重置它
重复显示可见和隐藏的实时消息使用一条状态消息,或经过测试且不会重复播报的设计

完成定义检查清单

  • 将每条消息归类为控件状态、状态消息、警报、日志、对话框内容或焦点内容
  • 尽可能提供可见信息
  • 将 role="status" 用于提示性更新;仅将 role="alert" 用于重要或有时效性的更新
  • 需要响应时,使用警报对话框和焦点管理
  • 在相关内容更新前提供实时区域——不依赖页面初次加载或固定延迟来保证播报
  • 不使用固定延迟后清除并重新插入的变通方案
  • 仅当行为与更新相符时才使用 aria-atomic、aria-relevant、aria-busy;了解 aria-relevant 的默认值为 additions text
  • 更新期间,区域始终不会从无障碍树中隐藏
  • 不将大型容器和交互控件用作实时区域
  • 减少或抑制快速、重复和过时的更新
  • 无论是否启用播报或播报是否遗漏,焦点管理都保持正确
  • 测试成功、错误、加载、重复操作和请求重叠等路径
  • 已测试:NVDA+Chrome、JAWS+Chrome、VoiceOver+Safari(若支持矩阵中包含 NVDA+Firefox,也须测试)

关键 WCAG 标准

  • 4.1.3 状态消息(AA)— 若状态更新未被播报,则属关键问题
  • 4.1.2 名称、角色、值(A)
  • 3.3.1 错误识别(A)
  • 3.3.3 错误建议(AA)
  • 2.4.3 焦点顺序(A)
  • 2.2.1 可调整计时(A)

参考资料

标准展望: WCAG 3.0 正在制定中。预计状态消息要求 将得到保留,并可能进一步扩展。 关注:https://www.w3.org/TR/wcag-3.0/

评论

支持 Markdown