Skip to content

ARIA Live Regions

当页面内容动态变化时(如通过 JavaScript 更新 DOM),屏幕阅读器默认不会播报这些变化。ARIA Live Regions 提供了一种机制,让辅助技术能够自动感知和播报动态内容更新。本节将详细介绍 aria-live 属性的三个级别(polite/assertive/off)、aria-atomic 属性、以及如何在实际项目中正确使用 Live Regions。

前置知识

阅读本节前,建议先了解:ARIA 状态与属性

为什么需要 Live Regions

现代 Web 应用大量使用 AJAX 和 JavaScript 动态更新页面内容。然而,屏幕阅读器用户无法自动感知这些变化:

  • 表单提交后显示的成功/错误消息
  • 购物车中商品数量的更新
  • 实时搜索结果
  • 通知消息的弹出

Live Regions 通过声明特定区域为"活跃区域",让辅助技术在区域内容变化时自动播报更新。

aria-live 属性

aria-live 是 Live Regions 的核心属性,它定义了辅助技术对内容更新的响应优先级。

说明播报时机
off不播报更新(默认值)不播报
polite礼貌模式当前播报完成后播报更新
assertive紧急模式立即中断当前播报
html
<!-- 礼貌模式:等屏幕阅读器当前播报完再播报 -->
<div aria-live="polite" aria-atomic="true">
  <!-- 表单提交后的反馈消息 -->
</div>

<!-- 紧急模式:立即播报,中断当前播报 -->
<div role="alert" aria-live="assertive">
  <!-- 错误提示、紧急通知 -->
</div>

<!-- 关闭:不播报更新 -->
<div aria-live="off">
  <!-- 动态内容不需要播报 -->
</div>

polite 模式

polite 模式是最常用的级别,适合大多数动态内容更新场景:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>aria-live polite 示例</title>
</head>
<body>
  <h1>搜索商品</h1>

  <form>
    <label for="search-input">搜索</label>
    <input type="search" id="search-input" />
    <button type="submit">搜索</button>
  </form>

  <!-- 礼貌模式的 live region -->
  <div aria-live="polite" aria-atomic="true" aria-label="搜索结果">
    <!-- JavaScript 动态更新此区域 -->
  </div>

  <script>
    const searchInput = document.getElementById('search-input');
    const liveRegion = document.querySelector('[aria-live="polite"]');

    searchInput.addEventListener('input', function() {
      const query = this.value.trim();

      if (query.length === 0) {
        liveRegion.textContent = '';
      } else {
        // 模拟搜索结果计数
        const count = Math.floor(Math.random() * 50) + 1;
        liveRegion.textContent = `找到 ${count} 个与"${query}"相关的结果`;
      }
    });
  </script>
</body>
</html>

assertive 模式

assertive 模式应谨慎使用,仅在真正紧急的消息中使用:

html
<!-- role="alert" 隐含 aria-live="assertive" 和 aria-atomic="true" -->
<div role="alert">
  <p>您的会话即将过期,请保存您的工作。</p>
</div>

<!-- 重要错误消息 -->
<div aria-live="assertive" aria-atomic="true">
  <p>支付失败:信用卡信息无效</p>
</div>

<!-- 网络断开提示 -->
<div aria-live="assertive" aria-atomic="true" role="alert">
  <p>网络连接已断开,部分功能不可用。</p>
</div>

assertive 模式使用原则

  • 仅用于需要用户立即注意的消息
  • 不要在频繁更新的内容上使用 assertive
  • 大量使用 assertive 会严重干扰屏幕阅读器用户体验
  • 错误消息通常不需要 assertive,polite 就足够了

role="alert"

role="alert" 是最常用的 Live Region 快捷方式,它隐含了:

  • aria-live="assertive"
  • aria-atomic="true"
html
<!-- 以下两种写法等效 -->
<div role="alert">操作成功!</div>
<div aria-live="assertive" aria-atomic="true">操作成功!</div>
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>表单验证 alert 示例</title>
  <style>
    .form-group { margin: 1rem 0; }
    .error-message { color: #d32f2f; margin-top: 0.25rem; }
    [role="alert"] { transition: opacity 0.3s; }
  </style>
</head>
<body>
  <form id="login-form">
    <div class="form-group">
      <label for="username">用户名</label>
      <input type="text" id="username" aria-describedby="username-error" />
      <div id="username-error" role="alert" aria-live="assertive"></div>
    </div>

    <div class="form-group">
      <label for="password">密码</label>
      <input type="password" id="password" aria-describedby="password-error" />
      <div id="password-error" role="alert" aria-live="assertive"></div>
    </div>

    <button type="submit">登录</button>
  </form>

  <script>
    document.getElementById('login-form').addEventListener('submit', function(e) {
      e.preventDefault();
      let hasError = false;

      // 验证用户名
      const username = document.getElementById('username');
      const usernameError = document.getElementById('username-error');

      if (!username.value.trim()) {
        usernameError.textContent = '用户名不能为空';
        username.setAttribute('aria-invalid', 'true');
        hasError = true;
      } else {
        usernameError.textContent = '';
        username.setAttribute('aria-invalid', 'false');
      }

      // 验证密码
      const password = document.getElementById('password');
      const passwordError = document.getElementById('password-error');

      if (!password.value.trim()) {
        passwordError.textContent = '密码不能为空';
        password.setAttribute('aria-invalid', 'true');
        hasError = true;
      } else if (password.value.length < 8) {
        passwordError.textContent = '密码至少需要 8 个字符';
        password.setAttribute('aria-invalid', 'true');
        hasError = true;
      } else {
        passwordError.textContent = '';
        password.setAttribute('aria-invalid', 'false');
      }
    });
  </script>
</body>
</html>

role="status"

role="status" 是另一种 Live Region 快捷方式,隐含了:

  • aria-live="polite"
  • aria-atomic="true"
html
<!-- 以下两种写法等效 -->
<div role="status">文件上传完成</div>
<div aria-live="polite" aria-atomic="true">文件上传完成</div>

aria-atomic 属性

aria-atomic 控制当区域内容变化时,辅助技术播报的范围:

说明
false(默认)只播报变化的部分
true播报整个区域的全部内容
html
<!-- aria-atomic="false"(默认):只播报变化部分 -->
<div aria-live="polite">
  <!-- 如果从 "3 项" 变为 "4 项",只播报 "4 项" -->
  <span class="count">4</span> 项在购物车中
</div>

<!-- aria-atomic="true":播报整个区域 -->
<div aria-live="polite" aria-atomic="true">
  <!-- 播报完整的 "4 项在购物车中" -->
  <span class="count">4</span> 项在购物车中
</div>

aria-relevant 属性

aria-relevant 更精细地控制哪些类型的变化应该触发播报:

说明
additions新增节点时播报(默认)
removals移除节点时播报
text文本内容变化时播报
all所有变化都播报(等效于 aria-atomic="true")
html
<!-- 只播报新增的列表项 -->
<div aria-live="polite" aria-relevant="additions">
  <ul id="chat-messages">
    <li>张三:你好!</li>
    <!-- 只播报新增的消息 -->
  </ul>
</div>

<!-- 播报文本变化和新增内容 -->
<div aria-live="polite" aria-relevant="text additions">
  <span>已上传 3/5 个文件</span>
</div>

实战示例:购物车实时更新

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>购物车实时更新</title>
  <style>
    .cart-item {
      display: flex;
      justify-content: space-between;
      padding: 0.5rem 0;
      border-bottom: 1px solid #eee;
    }
    .cart-total {
      font-weight: bold;
      font-size: 1.2rem;
      margin-top: 1rem;
    }
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }
  </style>
</head>
<body>
  <h1>购物车</h1>

  <!-- 屏幕阅读器专用的 live region -->
  <div id="cart-live" aria-live="polite" aria-atomic="true" class="sr-only">
    购物车中有 2 件商品,合计 ¥598.00
  </div>

  <div id="cart-items">
    <div class="cart-item">
      <span>无线蓝牙耳机</span>
      <span>¥299.00</span>
      <button onclick="removeItem(this, '无线蓝牙耳机', 299)">移除</button>
    </div>
    <div class="cart-item">
      <span>手机保护壳</span>
      <span>¥49.00</span>
      <button onclick="removeItem(this, '手机保护壳', 49)">移除</button>
    </div>
  </div>

  <div class="cart-total" id="cart-total">合计:¥348.00</div>

  <script>
    function removeItem(button, name, price) {
      button.closest('.cart-item').remove();
      updateCartInfo(name, price, false);
    }

    function updateCartInfo(name, price, isAdd) {
      const items = document.querySelectorAll('.cart-item');
      const count = items.length;
      let total = 0;
      items.forEach(item => {
        const priceText = item.querySelector('span:nth-child(2)').textContent;
        total += parseFloat(priceText.replace('¥', ''));
      });

      document.getElementById('cart-total').textContent = `合计:¥${total.toFixed(2)}`;

      const liveRegion = document.getElementById('cart-live');
      const action = isAdd ? '已添加' : '已移除';
      liveRegion.textContent = `${action} ${name}。购物车现有 ${count} 件商品,合计 ¥${total.toFixed(2)}`;
    }
  </script>
</body>
</html>

常见陷阱

1. Live Region 不播报已有内容

Live Region 只在内容变化时播报,页面加载时已有内容不会被播报:

html
<!-- 错误:页面加载后希望播报的内容不会被播报 -->
<div aria-live="polite">
  <p>欢迎回来,您有 3 条新消息。</p>
</div>

<!-- 正确:先设为空,再通过 JavaScript 设置内容 -->
<div aria-live="polite" id="welcome-msg"></div>
<script>
  setTimeout(() => {
    document.getElementById('welcome-msg').textContent = '欢迎回来,您有 3 条新消息。';
  }, 100);
</script>

2. 使用 textContent 更新更可靠

某些屏幕阅读器只在文本内容变化时触发播报,innerHTML 替换可能不被检测到:

html
<script>
  // 推荐:文本内容更新
  liveRegion.textContent = '新的消息内容';

  // 不推荐:innerHTML 替换可能不被检测到
  liveRegion.innerHTML = '<p>新的消息内容</p>';
</script>

3. 避免嵌套 Live Region

html
<!-- 错误:alert 已隐含 live,再嵌套会冲突 -->
<div role="alert">
  <div aria-live="polite">消息内容</div>
</div>

<!-- 正确:直接使用 role="alert" -->
<div role="alert">消息内容</div>

注意事项

  1. 不要过度使用 assertive:只在真正紧急的情况下使用
  2. Live Region 内容要简洁:播报内容应简明扼要
  3. 考虑 aria-busy:批量更新时先设置 aria-busy="true",更新完成后再设为 false
  4. 测试不同屏幕阅读器:各屏幕阅读器对 Live Region 的实现和支持程度不同

最佳实践

  • 使用 role="alert" 处理需要立即引起注意的错误和警告
  • 使用 role="status"aria-live="polite" 处理一般性的状态更新
  • 配合 aria-atomic="true" 确保播报完整信息而非片段
  • 使用 aria-busy 避免在批量更新时逐条播报中间状态
  • Live Region 的文本内容保持简洁,提供足够上下文即可

下一节

继续学习:焦点管理

参考链接