role 与 aria-* 无障碍
ARIA(Accessible Rich Internet Applications)是一套属性,用于增强 Web 内容和 Web 应用的无障碍性。它包括 role 属性和一系列 aria-* 属性,帮助辅助技术(如屏幕阅读器)理解非语义化或自定义组件的含义和行为。
前置知识
阅读本节前,建议先了解:data-* 自定义数据属性
基础概念
ARIA 第一规则
如果你使用的是原生 HTML 语义化标签(如
<button>,<nav>,<main>),不要使用 ARIA。只有在没有合适的语义化标签时才使用 ARIA。
换句话说:No ARIA is better than bad ARIA.(没有 ARIA 比错误的 ARIA 更好)
为什么需要 ARIA
HTML 的语义化标签无法覆盖所有 UI 组件。对于自定义组件(如手风琴、标签页、模态框等),需要 ARIA 来传达语义:
html
<!-- 不需要 ARIA:原生按钮 -->
<button>提交</button>
<!-- 需要 ARIA:自定义按钮 -->
<div role="button" tabindex="0">提交</div>
<!-- 不需要 ARIA:原生导航 -->
<nav aria-label="主导航">...</nav>
<!-- 需要 ARIA:自定义标签页 -->
<div role="tablist">
<div role="tab" tabindex="0" aria-selected="true">标签1</div>
<div role="tab" tabindex="-1" aria-selected="false">标签2</div>
</div>语法与使用
role 属性
role 属性为元素定义一个语义角色,告诉辅助技术这个元素"是什么":
html
<!-- 常用角色 -->
<div role="button">按钮</div>
<div role="dialog" aria-modal="true">对话框</div>
<div role="alert" aria-live="assertive">警告</div>
<div role="navigation" aria-label="面包屑">...</div>
<div role="search">搜索区域</div>
<div role="tablist">标签列表</div>
<div role="tabpanel">标签面板</div>
<div role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">50%</div>aria-* 属性分类
| 分类 | 属性 | 用途 |
|---|---|---|
| 标识 | aria-label, aria-labelledby | 提供可访问名称 |
| 状态 | aria-expanded, aria-selected, aria-checked | 当前状态 |
| 属性 | aria-hidden, aria-disabled, aria-required | 特性 |
| 实时区域 | aria-live, aria-atomic, aria-relevant | 动态内容 |
| 关联 | aria-controls, aria-describedby, aria-owns | 元素关系 |
| 键盘 | aria-keyshortcuts | 快捷键 |
aria-label 与 aria-labelledby
html
<!-- aria-label:直接提供文本名称 -->
<button aria-label="关闭对话框">×</button>
<!-- aria-labelledby:引用页面上的可见元素 -->
<div>
<h2 id="dialog-title">确认删除</h2>
<p>确定要删除吗?</p>
</div>
<div role="dialog" aria-labelledby="dialog-title">
...
</div>
<!-- 两者结合使用 -->
<button aria-label="关闭" aria-labelledby="close-icon">
<span id="close-icon">×</span>
</button>详细说明
常用 ARIA 模式
1. 展开/收起
html
<button aria-expanded="false" aria-controls="panel1">
显示详情
</button>
<div id="panel1" hidden>
<p>隐藏的详细内容</p>
</div>
<script>
const btn = document.querySelector('[aria-expanded]');
const panel = document.getElementById('panel1');
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', !expanded);
panel.hidden = expanded;
});
</script>2. 标签页
html
<div role="tablist" aria-label="设置选项卡">
<button role="tab" aria-selected="true" id="tab-1" aria-controls="panel-1">基本设置</button>
<button role="tab" aria-selected="false" id="tab-2" aria-controls="panel-2">高级设置</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<p>基本设置内容</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<p>高级设置内容</p>
</div>3. Toast 通知
html
<!-- assertive:立即通知(错误、警告) -->
<div role="alert" aria-live="assertive">操作失败,请重试。</div>
<!-- polite:空闲时通知(成功、信息) -->
<div aria-live="polite">保存成功。</div>
<!-- atomic:整块更新通知 -->
<div aria-live="polite" aria-atomic="true">
<p>文件上传进度:50%</p>
</div>4. 加载状态
html
<button aria-busy="true" aria-disabled="true">
<span aria-hidden="true">⏳</span> 加载中...
</button>
<div role="progressbar" aria-valuenow="30" aria-valuemin="0" aria-valuemax="100" aria-label="上传进度">
30%
</div>实战示例
可访问的模态对话框
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ARIA 对话框示例</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; padding: 2rem; }
.overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
display: none; align-items: center; justify-content: center;
}
.overlay.active { display: flex; }
.modal {
background: #fff; padding: 2rem; border-radius: 12px;
max-width: 400px; width: 90%;
}
.modal h2 { margin-bottom: 1rem; }
.modal-close {
float: right; background: none; border: none;
font-size: 1.5rem; cursor: pointer; color: #666;
}
.btn { padding: 0.6rem 1.5rem; border: none; border-radius: 6px; cursor: pointer; }
.btn-primary { background: #3b82f6; color: #fff; }
button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
</style>
</head>
<body>
<h1>ARIA 对话框示例</h1>
<button class="btn btn-primary" id="open-btn">打开对话框</button>
<div class="overlay" id="overlay">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<button class="modal-close" aria-label="关闭对话框" onclick="closeDialog()">×</button>
<h2 id="dialog-title">确认操作</h2>
<p id="dialog-desc">您确定要执行此操作吗?此操作无法撤销。</p>
<div style="margin-top:1.5rem;display:flex;justify-content:flex-end;gap:0.5rem;">
<button class="btn" style="background:#f1f5f9" onclick="closeDialog()">取消</button>
<button class="btn btn-primary" onclick="closeDialog()">确认</button>
</div>
</div>
</div>
<script>
let lastFocused = null;
const overlay = document.getElementById('overlay');
document.getElementById('open-btn').addEventListener('click', () => {
lastFocused = document.activeElement;
overlay.classList.add('active');
overlay.querySelector('h2').focus();
});
function closeDialog() {
overlay.classList.remove('active');
if (lastFocused) lastFocused.focus();
}
overlay.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDialog();
if (e.key === 'Tab') {
const focusable = overlay.querySelectorAll('button:not([disabled])');
if (e.shiftKey) {
if (document.activeElement === focusable[0]) {
e.preventDefault();
focusable[focusable.length - 1].focus();
}
} else {
if (document.activeElement === focusable[focusable.length - 1]) {
e.preventDefault();
focusable[0].focus();
}
}
}
});
</script>
</body>
</html>注意事项
常见错误
- 过度使用 ARIA:原生元素(button, a, input)不需要添加 role
- 忘记 aria-expanded 更新:展开/收起状态必须同步更新
- aria-hidden 不等于 hidden:aria-hidden 只影响辅助技术,不影响视觉
- 角色不匹配:确保 ARIA 角色与元素行为一致
html
<!-- 错误:button 已经有隐式 role="button" -->
<button role="button">提交</button> <!-- 冗余 -->
<!-- 正确 -->
<button>提交</button>
<!-- 错误:aria-hidden 后仍有可聚焦元素 -->
<div aria-hidden="true">
<button>不能聚焦的按钮</button> <!-- 矛盾 -->
</div>最佳实践
- 首选语义化 HTML:只在必要时使用 ARIA
- 所有交互元素可聚焦:确保 Tab 键可以到达
- 提供可访问名称:使用 aria-label 或可见文本
- 状态同步更新:aria-expanded、aria-selected 等要实时更新
- 测试无障碍性:使用屏幕阅读器和 axe 工具测试
下一节
继续学习:hidden 隐藏