ARIA 角色(role)
ARIA 角色用于向辅助技术(如屏幕阅读器)描述 HTML 元素的用途和语义。当原生 HTML 元素无法满足复杂的交互需求时,ARIA 角色可以弥补语义的缺失,使自定义组件对辅助技术变得可理解。本节将系统介绍 landmark roles、widget roles 和 document roles 三大类角色,帮助你在构建复杂 UI 组件时做出正确的角色选择。
前置知识
阅读本节前,建议先了解:无障碍概述与原则
什么是 ARIA 角色
ARIA(Accessible Rich Internet Applications)是 W3C 制定的一套规范,用于弥补 HTML 在表达动态内容和高级 UI 组件时的语义不足。role 属性是 ARIA 的核心部分之一,它告诉辅助技术某个元素扮演什么"角色"。
html
<!-- 原生 HTML 元素已有隐含角色,无需重复声明 -->
<nav role="navigation"> <!-- 冗余!nav 自带 navigation 角色 -->
<a href="/">首页</a>
</nav>
<!-- 自定义元素需要显式声明角色 -->
<div role="navigation" aria-label="主导航">
<a href="/">首页</a>
</div>第一规则:No ARIA is better than bad ARIA
如果原生 HTML 元素已经具有你需要的语义和功能,就不要使用 ARIA。ARIA 只应在原生 HTML 无法满足需求时使用。
Landmark Roles(地标角色)
Landmark roles 用于标识页面的主要区域,帮助屏幕阅读器用户快速导航到页面的不同部分。
| Landmark Role | 对应 HTML 元素 | 说明 |
|---|---|---|
banner | <header>(页面级) | 页面或区域的横幅区域 |
navigation | <nav> | 导航区域 |
main | <main> | 页面的主要内容区域 |
complementary | <aside> | 补充内容区域(如侧边栏) |
contentinfo | <footer>(页面级) | 页面或区域的页脚信息 |
search | <search> | 搜索功能区域 |
form | <form>(有 name 属性时) | 表单区域 |
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Landmark Roles 示例</title>
</head>
<body>
<!-- banner: 页面顶部横幅 -->
<!-- <header> 隐含 role="banner"(仅当是 body 的直接子元素时) -->
<header role="banner">
<div class="logo">我的网站</div>
</header>
<!-- navigation: 导航区域 -->
<!-- <nav> 隐含 role="navigation" -->
<nav role="navigation" aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于我们</a></li>
<li><a href="/contact">联系方式</a></li>
</ul>
</nav>
<!-- main: 主要内容区域 -->
<!-- <main> 隐含 role="main",每个页面只应有一个 -->
<main role="main" id="main-content">
<h1>欢迎访问</h1>
<p>这是页面的主要内容区域。</p>
</main>
<!-- complementary: 补充内容(侧边栏) -->
<!-- <aside> 隐含 role="complementary" -->
<aside role="complementary" aria-label="相关文章">
<h2>推荐阅读</h2>
<ul>
<li><a href="/article/1">如何学习 HTML</a></li>
<li><a href="/article/2">CSS 入门指南</a></li>
</ul>
</aside>
<!-- contentinfo: 页脚信息 -->
<!-- <footer> 隐含 role="contentinfo"(仅当是 body 的直接子元素时) -->
<footer role="contentinfo">
<p>版权所有 2024</p>
</footer>
</body>
</html>多个同类型 Landmark 的处理
当页面有多个相同类型的 landmark 时,必须使用 aria-label 区分:
html
<!-- 页面有多个导航区域,需要 aria-label 区分 -->
<nav aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
</ul>
</nav>
<main>
<h1>产品页面</h1>
<!-- 这是页面内的面包屑导航 -->
<nav aria-label="面包屑">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li>当前产品</li>
</ol>
</nav>
<!-- 产品详情内的页内导航 -->
<nav aria-label="产品详情导航">
<ul>
<li><a href="#description">描述</a></li>
<li><a href="#reviews">评价</a></li>
</ul>
</nav>
</main>
<footer>
<nav aria-label="页脚导航">
<ul>
<li><a href="/privacy">隐私政策</a></li>
<li><a href="/terms">服务条款</a></li>
</ul>
</nav>
</footer>Widget Roles(组件角色)
Widget roles 用于描述交互式 UI 组件,如按钮、复选框、对话框、选项卡等。
常用 Widget Roles
| Role | 用途 | 对应原生元素 |
|---|---|---|
button | 可点击的按钮 | <button> |
link | 超链接 | <a href> |
checkbox | 复选框 | <input type="checkbox"> |
radio | 单选按钮 | <input type="radio"> |
textbox | 文本输入框 | <input type="text"> |
searchbox | 搜索框 | <input type="search"> |
combobox | 组合框(下拉输入) | <input> + <datalist> |
listbox | 列表框 | <select> / <datalist> |
option | 选项 | <option> |
slider | 滑块 | <input type="range"> |
spinbutton | 数字调节器 | <input type="number"> |
switch | 开关 | 无直接对应(可用 checkbox + role="switch") |
tab | 选项卡标签 | 无直接对应 |
tabpanel | 选项卡面板 | 无直接对应 |
tablist | 选项卡列表 | 无直接对应 |
dialog | 对话框 | <dialog> |
alertdialog | 警告对话框 | <dialog> |
menu | 菜单 | <menu> |
menubar | 菜单栏 | 无直接对应 |
menuitem | 菜单项 | <menuitem> |
tooltip | 工具提示 | <title> |
progressbar | 进度条 | <progress> |
meter | 度量器 | <meter> |
tree | 树形控件 | 无直接对应 |
treeitem | 树形项 | 无直接对应 |
grid | 网格 | <table> |
gridcell | 网格单元格 | <td> |
选项卡组件示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>选项卡组件</title>
<style>
/* 选项卡样式 */
.tablist {
display: flex;
border-bottom: 2px solid #ddd;
list-style: none;
margin: 0;
padding: 0;
}
.tablist li { margin: 0; }
.tablist [role="tab"] {
padding: 0.5rem 1rem;
border: 1px solid transparent;
border-bottom: none;
background: #f5f5f5;
cursor: pointer;
}
/* 选中的选项卡样式 */
.tablist [role="tab"][aria-selected="true"] {
border-color: #ddd #ddd #fff;
background: #fff;
border-bottom: 2px solid #fff;
margin-bottom: -2px;
}
.tabpanel {
padding: 1rem;
border: 1px solid #ddd;
border-top: none;
}
/* 隐藏非活动面板 */
[role="tabpanel"][hidden] { display: none; }
</style>
</head>
<body>
<div class="tabs">
<!-- 选项卡列表 -->
<div role="tablist" aria-label="产品信息选项卡">
<button role="tab" id="tab-1"
aria-selected="true" aria-controls="panel-1">
描述
</button>
<button role="tab" id="tab-2"
aria-selected="false" aria-controls="panel-2" tabindex="-1">
规格
</button>
<button role="tab" id="tab-3"
aria-selected="false" aria-controls="panel-3" tabindex="-1">
评价
</button>
</div>
<!-- 选项卡面板 -->
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<h2>产品描述</h2>
<p>这是一款高品质无线蓝牙耳机...</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<h2>产品规格</h2>
<ul>
<li>蓝牙版本:5.3</li>
<li>续航时间:30小时</li>
<li>重量:200g</li>
</ul>
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
<h2>用户评价</h2>
<p>平均评分:4.8/5.0</p>
</div>
</div>
<script>
// 选项卡交互逻辑
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');
tabs.forEach(tab => {
tab.addEventListener('click', activateTab);
tab.addEventListener('keydown', handleTabKeydown);
});
function activateTab(event) {
// 停用所有选项卡
tabs.forEach(t => {
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
// 隐藏所有面板
panels.forEach(p => p.setAttribute('hidden', ''));
// 激活当前选项卡
const activeTab = event.currentTarget;
activeTab.setAttribute('aria-selected', 'true');
activeTab.setAttribute('tabindex', '0');
activeTab.focus();
// 显示对应面板
const panelId = activeTab.getAttribute('aria-controls');
document.getElementById(panelId).removeAttribute('hidden');
}
function handleTabKeydown(event) {
const tabsArray = Array.from(tabs);
const currentIndex = tabsArray.indexOf(event.currentTarget);
let nextIndex;
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
nextIndex = (currentIndex + 1) % tabsArray.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
nextIndex = (currentIndex - 1 + tabsArray.length) % tabsArray.length;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = tabsArray.length - 1;
break;
default:
return;
}
event.preventDefault();
tabsArray[nextIndex].focus();
}
</script>
</body>
</html>开关(Switch)组件示例
html
<!-- 使用 role="switch" 创建开关组件 -->
<div
role="switch"
tabindex="0"
aria-checked="false"
aria-label="深色模式"
onclick="toggleSwitch(this)"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSwitch(this)}"
>
深色模式:关闭
</div>
<script>
function toggleSwitch(el) {
const isChecked = el.getAttribute('aria-checked') === 'true';
el.setAttribute('aria-checked', String(!isChecked));
el.textContent = '深色模式:' + (!isChecked ? '开启' : '关闭');
}
</script>Document Roles(文档角色)
Document roles 用于描述内容的结构化组织方式,通常不直接提供交互功能。
| Document Role | 用途 | 说明 |
|---|---|---|
document | 文档根节点 | <body> 隐含此角色 |
article | 独立内容 | <article> 隐含此角色 |
section | 内容分区 | <section> 隐含此角色(有标题时) |
heading | 标题 | <h1>~<h6> 隐含此角色,aria-level 表示层级 |
group | 分组 | <fieldset>、<optgroup> 隐含此角色 |
list | 列表 | <ul>、<ol> 隐含此角色 |
listitem | 列表项 | <li> 隐含此角色 |
note | 注释 | 用于脚注、旁注等 |
definition | 术语定义 | 无直接对应 |
math | 数学表达式 | 用于数学公式区域 |
timer | 计时器 | 用于倒计时、计时器等 |
status | 状态消息 | 用于提示当前状态 |
log | 日志 | 用于实时更新的日志区域 |
marquee | 滚动内容 | 用于自动滚动的文本 |
feed | 信息流 | 用于动态更新的内容列表 |
heading 角色与 aria-level
html
<!-- 原生 heading 元素隐含 role="heading" 和 aria-level -->
<h1>相当于</h1> <!-- role="heading" aria-level="1" -->
<h2>相当于</h2> <!-- role="heading" aria-level="2" -->
<h3>相当于</h3> <!-- role="heading" aria-level="3" -->
<!-- 自定义 heading 需要显式指定 aria-level -->
<div role="heading" aria-level="2">自定义二级标题</div>
<!-- 注意:不要滥用 heading 角色 -->
<div role="heading" aria-level="1">
<!-- 这不应该出现在 h2 后面,会破坏层级 -->
</div>note 角色示例
html
<article>
<h1>量子计算入门</h1>
<p>量子计算是一种利用量子力学原理的计算方式。</p>
<!-- 使用 note 角色标记注释 -->
<aside role="note" aria-label="脚注">
<p>本文基于 2024 年的研究进展编写,内容可能随技术发展而更新。</p>
</aside>
</article>feed 角色(信息流)
html
<!-- 使用 feed 角色标记动态更新的信息流 -->
<section role="feed" aria-label="最新动态" aria-busy="false">
<article>
<h2>新功能发布</h2>
<p>我们刚刚推出了全新的搜索功能...</p>
<time datetime="2024-01-15">2024年1月15日</time>
</article>
<article>
<h2>系统维护通知</h2>
<p>本周六凌晨 2:00-4:00 将进行系统维护...</p>
<time datetime="2024-01-14">2024年1月14日</time>
</article>
<!-- 加载更多内容时,标记 feed 为忙碌状态 -->
<!-- aria-busy="true" 时,屏幕阅读器会暂停播报新内容 -->
</section>抽象角色
有些角色是"抽象"的,不能直接使用,只能被子角色继承:
| 抽象角色 | 说明 | 子角色示例 |
|---|---|---|
command | 命令 | button、link、menuitem |
composite | 复合组件 | grid、listbox、menu、tablist、tree |
input | 输入组件 | checkbox、radio、textbox、slider |
landmark | 地标 | banner、navigation、main、search |
range | 范围值 | progressbar、slider、spinbutton |
roletype | 所有角色的基类 | 所有角色 |
section | 文档分区 | section、article、group |
sectionhead | 分区标题 | heading、tab |
widget | 交互组件 | 所有交互式角色 |
window | 窗口 | dialog、alertdialog |
注意事项
- 不要覆盖原生语义:如果
<nav>已经有navigation角色,就不需要再写role="navigation" - 避免角色冲突:确保角色与元素的用途匹配,不要在
<div>上使用role="link"(应使用<a>) - 考虑浏览器支持:大部分 ARIA 角色在现代浏览器中支持良好,但某些新角色可能需要 polyfill
- 始终测试辅助技术:不同屏幕阅读器对 ARIA 角色的解读可能不同
最佳实践
- 优先使用原生 HTML 元素,仅在必要时使用 ARIA 角色
- 为自定义组件使用合适的 ARIA 角色,确保辅助技术能正确理解其用途
- 同一页面中存在多个相同类型的 landmark 时,使用
aria-label区分 - 使用
aria-selected、aria-expanded、aria-checked等状态属性配合 widget 角色 - 为自定义组件实现完整的键盘交互,与角色的预期行为一致
下一节
继续学习:ARIA 状态与属性