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>注意事项
- 不要过度使用 assertive:只在真正紧急的情况下使用
- Live Region 内容要简洁:播报内容应简明扼要
- 考虑 aria-busy:批量更新时先设置
aria-busy="true",更新完成后再设为false - 测试不同屏幕阅读器:各屏幕阅读器对 Live Region 的实现和支持程度不同
最佳实践
- 使用
role="alert"处理需要立即引起注意的错误和警告 - 使用
role="status"或aria-live="polite"处理一般性的状态更新 - 配合
aria-atomic="true"确保播报完整信息而非片段 - 使用
aria-busy避免在批量更新时逐条播报中间状态 - Live Region 的文本内容保持简洁,提供足够上下文即可
下一节
继续学习:焦点管理