HTML 可访问性与 ARIA

深入理解 ARIA 的三大属性、ARIA 使用原则、live regions、常见 ARIA 模式,以及可访问性树与 DOM 树的关系。

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)、navigationmaincomplementary(aside)、contentinfo(footer)、search

UI 角色:标识 UI 组件类型

  • buttoncheckboxradiotextboxslidertablistmenutree

结构角色:标识内容的结构类型

  • articleregionheadinglistlistitemtable

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 可以查看可访问性树:

  1. 在 Elements 面板中选中元素
  2. 在右侧 Accessibility 面板查看该元素的可访问性信息
  3. 可以看到 computed properties(角色、名称、值)

延展阅读