Skip to content

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()">&times;</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>

注意事项

常见错误

  1. 过度使用 ARIA:原生元素(button, a, input)不需要添加 role
  2. 忘记 aria-expanded 更新:展开/收起状态必须同步更新
  3. aria-hidden 不等于 hidden:aria-hidden 只影响辅助技术,不影响视觉
  4. 角色不匹配:确保 ARIA 角色与元素行为一致
html
<!-- 错误:button 已经有隐式 role="button" -->
<button role="button">提交</button> <!-- 冗余 -->

<!-- 正确 -->
<button>提交</button>

<!-- 错误:aria-hidden 后仍有可聚焦元素 -->
<div aria-hidden="true">
  <button>不能聚焦的按钮</button> <!-- 矛盾 -->
</div>

最佳实践

  1. 首选语义化 HTML:只在必要时使用 ARIA
  2. 所有交互元素可聚焦:确保 Tab 键可以到达
  3. 提供可访问名称:使用 aria-label 或可见文本
  4. 状态同步更新:aria-expanded、aria-selected 等要实时更新
  5. 测试无障碍性:使用屏幕阅读器和 axe 工具测试

下一节

继续学习:hidden 隐藏

参考链接