Skip to content

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命令buttonlinkmenuitem
composite复合组件gridlistboxmenutablisttree
input输入组件checkboxradiotextboxslider
landmark地标bannernavigationmainsearch
range范围值progressbarsliderspinbutton
roletype所有角色的基类所有角色
section文档分区sectionarticlegroup
sectionhead分区标题headingtab
widget交互组件所有交互式角色
window窗口dialogalertdialog

注意事项

  1. 不要覆盖原生语义:如果 <nav> 已经有 navigation 角色,就不需要再写 role="navigation"
  2. 避免角色冲突:确保角色与元素的用途匹配,不要在 <div> 上使用 role="link"(应使用 <a>
  3. 考虑浏览器支持:大部分 ARIA 角色在现代浏览器中支持良好,但某些新角色可能需要 polyfill
  4. 始终测试辅助技术:不同屏幕阅读器对 ARIA 角色的解读可能不同

最佳实践

  • 优先使用原生 HTML 元素,仅在必要时使用 ARIA 角色
  • 为自定义组件使用合适的 ARIA 角色,确保辅助技术能正确理解其用途
  • 同一页面中存在多个相同类型的 landmark 时,使用 aria-label 区分
  • 使用 aria-selectedaria-expandedaria-checked 等状态属性配合 widget 角色
  • 为自定义组件实现完整的键盘交互,与角色的预期行为一致

下一节

继续学习:ARIA 状态与属性

参考链接