HTML 可访问性与 ARIA
一、从一个真实场景开始
想象你是盲人用户,使用屏幕阅读器浏览网页。当你按 Tab 键从一个可交互元素跳到另一个时,屏幕阅读器会朗读元素的信息。但如果页面上有一个自定义下拉菜单,你按回车打开后,屏幕阅读器可能完全不知道菜单已经打开,因为菜单的打开状态没有用任何方式表达给辅助技术。
这就是 ARIA 要解决的问题:在没有原生 HTML 语义的情况下,为辅助技术提供足够的信息。
二、ARIA 的三大属性
2.1 role 属性
role 属性定义了元素的角色——它在 UI 中承担什么功能:
<!-- 角色示例 -->
<div role="button">点击我</div>
<div role="checkbox" aria-checked="true">同意条款</div>
<div role="dialog" aria-modal="true">模态框</div>
<nav role="navigation">导航</nav>
role 的分类:
Landmark 角色:标识页面的大区域
banner(header)、navigation、main、complementary(aside)、contentinfo(footer)、search
UI 角色:标识 UI 组件类型
button、checkbox、radio、textbox、slider、tablist、menu、tree
结构角色:标识内容的结构类型
article、region、heading、list、listitem、table
2.2 aria-* 状态属性
状态属性描述元素的当前状态:
<!-- 选中状态 -->
<div role="checkbox" aria-checked="true">已选中</div>
<!-- 展开状态 -->
<details>
<summary aria-expanded="false">点击展开</summary>
<!-- 展开时 JavaScript 更新为 true -->
</details>
<!-- 禁用状态 -->
<button aria-disabled="true">不可用</button>
<!-- 隐藏状态 -->
<div aria-hidden="true">屏幕阅读器忽略</div>
2.3 aria-* 属性
属性描述元素的额外特征:
<!-- 标签关联 -->
<button aria-label="关闭" aria-labelledby="titleId">X</button>
<div id="titleId" class="visually-hidden">弹窗标题</div>
<!-- 描述关联 -->
<input aria-describedby="hintId">
<span id="hintId">请输入 8-20 位密码</span>
<!-- 当前值 -->
<div role="slider" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
三、ARIA 使用原则
3.1 优先使用原生 HTML
这是 ARIA 最重要的原则:如果原生 HTML 能实现同样的功能,就不要用 ARIA。
<!-- ✅ 正确:使用原生语义按钮 -->
<button>提交</button>
<!-- ❌ 不推荐:自定义按钮需要额外的 ARIA -->
<div role="button" tabindex="0">提交</div>
<!-- ✅ 正确:使用原生复选框 -->
<input type="checkbox">
<!-- ❌ 不推荐:自定义复选框 -->
<div role="checkbox" aria-checked="false" tabindex="0">
原生 HTML 元素自动获得完整的可访问性语义:
- 键盘可访问(Tab 导航、Enter/Space 激活)
- 屏幕阅读器自动识别
- 浏览器提供默认行为
3.2 ARIA 不会让元素可键盘访问
添加 role 不会自动让元素可聚焦。你需要同时添加 tabindex:
<!-- ❌ 错误:role="button" 但不可 Tab 聚焦 -->
<div role="button">点击我</div>
<!-- ✅ 正确:role + tabindex -->
<div role="button" tabindex="0">点击我</div>
<!-- ✅ 更好的做法:使用原生 button -->
<button>点击我</button>
3.3 动态内容需要更新 ARIA
当 UI 状态变化时,必须更新 ARIA 属性:
// 切换开关状态
toggle.addEventListener('click', () => {
const isOn = toggle.getAttribute('aria-checked') === 'true';
toggle.setAttribute('aria-checked', !isOn);
// 同时更新视觉状态(如果有自定义样式)
toggle.classList.toggle('on', !isOn);
});
// 打开模态框
openBtn.addEventListener('click', () => {
modal.setAttribute('aria-hidden', 'false');
modal.setAttribute('aria-modal', 'true');
// 聚焦到模态框内部
closeBtn.focus();
});
四、Live Regions
4.1 什么是 Live Regions
Live regions 是页面中动态更新内容的容器。当内容变化时,屏幕阅读器会自动 announce 更新,而不需要用户主动导航到那里:
<!-- 简单例子:进度通知 -->
<div aria-live="polite" aria-atomic="true">
<!-- 当这个 div 内的文本变化时,屏幕阅读器会在用户空闲时 announce -->
<span id="status">正在上传...</span>
</div>
<script>
// 模拟上传进度
status.textContent = '上传 25%...';
setTimeout(() => { status.textContent = '上传 50%...'; }, 1000);
</script>
4.2 aria-live 的值
<!-- polite:等待用户空闲时 announce -->
<div aria-live="polite">状态更新</div>
<!-- assertive:立即打断用户 announce(慎用)-->
<div aria-live="assertive">错误!这很紧急</div>
<!-- off:不 announce(默认值)-->
<div aria-live="off">静默更新</div>
4.3 aria-atomic
aria-atomic 告诉屏幕阅读器在区域更新时,是 announce 整个区域还是只 announce 变化的部分:
<!-- atomic="false":只 announce 变化的文本 -->
<div aria-live="polite" aria-atomic="false">
<span>步骤 1/3:</span>
<span id="step">正在验证</span>
<!-- 只有"正在验证"会被 announce -->
</div>
<!-- atomic="true":每次变化都 announce 整个区域 -->
<div aria-live="polite" aria-atomic="true">
<span>步骤 1/3:</span>
<span id="step">正在验证</span>
<!-- 整个内容会被 announce:"步骤 1/3 正在验证" -->
</div>
4.4 aria-relevant
aria-relevant 指定哪种变化需要 announce:
<!-- 默认:all,表示任何变化都 announce -->
<div aria-live="polite">
<!-- 只 announce 添加和移除 -->
<div aria-live="polite" aria-relevant="additions removals">
<!-- 只 announce 文本变化 -->
<div aria-live="polite" aria-relevant="text">
五、常见 ARIA 模式
5.1 Dialog/Modal
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
>
<h2 id="dialog-title">确认删除</h2>
<p id="dialog-desc">此操作不可撤销</p>
<button>取消</button>
<button>确认删除</button>
</div>
<script>
const dialog = document.querySelector('[role="dialog"]');
// 打开时:设置 focus trap
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
// 阻止焦点逃出模态框
}
});
// 关闭时:返回触发元素
closeBtn.addEventListener('click', () => {
dialog.setAttribute('aria-hidden', 'true');
triggerElement.focus();
});
</script>
5.2 Tabs
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
Tab 1
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Tab 2
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
Panel 1 content...
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
Panel 2 content...
</div>
<script>
// Tab 切换逻辑
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// 更新 aria-selected
tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
tab.setAttribute('aria-selected', 'true');
// 切换面板
panels.forEach(p => p.hidden = true);
document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
});
});
</script>
5.3 Menu/Submenu
<ul role="menubar">
<li role="none">
<button role="menuitem" aria-haspopup="true" aria-expanded="false">
文件
</button>
<ul role="menu" hidden>
<li role="none">
<button role="menuitem">新建</button>
</li>
<li role="none">
<button role="menuitem">打开</button>
</li>
</ul>
</li>
</ul>
六、可访问性树与 DOM 树
6.1 可访问性树的生成
浏览器在解析 DOM 树后,会生成一棵平行的可访问性树(Accessibility Tree):
DOM 树 可访问性树
<html> [Document]
├── <body> [Content]
│ ├── <header> [Banner landmark]
│ │ └── <nav> [Navigation landmark]
│ │ └── <ul> [List]
│ │ └── <li> [Listitem]
│ │ └── <a> [Link: "首页", url]
│ ├── <main> [Main landmark]
│ │ └── <article> [Article]
│ │ └── <button> [Button: "删除", disabled]
6.2 辅助技术如何使用可访问性树
屏幕阅读器:
- 朗读焦点元素的角色、名称、状态
- 提供键盘快捷键在 landmarks 间跳转
- 通过虚拟光标浏览页面内容
屏幕放大器:
- 跟随焦点放大显示
- 高亮当前焦点区域
语音控制软件:
- 识别可访问性树中的可交互元素
- 允许用户用语音命令激活元素
6.3 调试可访问性树
Chrome DevTools 可以查看可访问性树:
- 在 Elements 面板中选中元素
- 在右侧 Accessibility 面板查看该元素的可访问性信息
- 可以看到 computed properties(角色、名称、值)