Files
2026-07-13 21:36:46 +08:00

6.1 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
aria-patterns-reference ARIA 模式与键盘交互参考
type
reference

ARIA 模式与键盘交互参考

地标角色

每个页面都应包含以下地标:

<header role="banner">         <!-- 网站页眉——每页一次 -->
<nav role="navigation">         <!-- 导航区域——可含多个,用 aria-label 区分 -->
<main role="main">              <!-- 主要内容——每页一次 -->
<aside role="complementary">    <!-- 侧边栏——相关但非必需 -->
<footer role="contentinfo">     <!-- 网站页脚——每页一次 -->
<form role="search">            <!-- 搜索表单 -->

语义 HTML 等效写法: <header><nav><main><aside><footer> 已隐含相应角色,无需再重复添加显式的 role 属性。

动态区域

何时使用

模式 属性 使用场景
礼貌式 aria-live="polite" 吐司通知、状态更新、搜索结果数量
强句式 aria-live="assertive" 错误消息、紧急提示、表单验证错误
状态 role="status" 加载指示器、进度更新
警报 role="alert" 错误对话框、时效性警告
日志 role="log" 聊天消息、活动流
计时器 role="timer" 倒计时器

实现方式

<!-- 吐司通知 -->
<div aria-live="polite" aria-atomic="true">
  <!-- 在此动态注入吐司内容 -->
</div>

<!-- 表单验证错误 -->
<div aria-live="assertive" role="alert">
  <p>请输入有效的电子邮件地址。</p>
</div>

<!-- 加载状态 -->
<div role="status" aria-live="polite">
  正在加载结果……
</div>

关键规则: 动态区域容器必须在内容注入之前就已存在于 DOM 中。向新创建的元素添加 aria-live 将不会触发语音播报。

焦点管理

焦点陷阱(模态框)

// 将焦点限制在模态框内
const modal = document.querySelector('[role="dialog"]');
const focusable = modal.querySelectorAll(
  'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];

modal.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
  if (e.key === 'Escape') closeModal();
});

焦点恢复

// 打开模态框前保存焦点
const trigger = document.activeElement;
openModal();

// 关闭时恢复焦点
function closeModal() {
  modal.hidden = true;
  trigger.focus();
}

跳转链接

<a href="#main-content" class="skip-link">跳转到主内容</a>
<!-- ……导航…… -->
<main id="main-content" tabindex="-1">
.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 999;
}
.skip-link:focus {
  left: 10px;
  top: 10px;
  background: #000;
  color: #fff;
  padding: 8px 16px;
}

键盘交互模式

标签页

Tab          → 移入标签列表,然后移入标签面板
左右方向键   → 切换标签页
Home         → 第一个标签页
End          → 最后一个标签页
<div role="tablist" aria-label="设置">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">通用</button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">安全</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">……</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>……</div>

组合框 / 自动补全

下方向键   → 打开列表 / 下一选项
上方向键   → 上一选项
Enter      → 选中选项
Escape     → 关闭列表
输入文字   → 过滤选项

菜单

Enter/空格  → 激活项目
下方向键   → 下一项目
上方向键   → 上一项目
右方向键   → 打开子菜单
左方向键   → 关闭子菜单
Escape     → 关闭菜单

手风琴式折叠面板

Enter/空格  → 切换分区
下方向键   → 下一标题
上方向键   → 上一标题
Home       → 第一个标题
End        → 最后一个标题

框架特定的 ARIA

React

// 播报路由变化(SPA
<div aria-live="polite" className="sr-only">
  {`已导航到 ${pageTitle}`}
</div>

// 带有无障碍提示的错误边界
<div role="alert">
  <h2>出错了</h2>
  <p>{error.message}</p>
</div>

Vue

<!-- 播报动态内容 -->
<div aria-live="polite">
  <p v-if="results.length">找到 {{ results.length }} 条结果</p>
</div>

<!-- 无障碍开关 -->
<button
  :aria-expanded="isOpen"
  :aria-controls="panelId"
  @click="toggle"
>
  {{ isOpen ? '收起' : '展开' }}
</button>

Angular

<!-- cdkTrapFocus 用于模态框 -->
<div cdkTrapFocus cdkTrapFocusAutoCapture role="dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">确认操作</h2>
</div>

<!-- LiveAnnouncer 服务 -->
<!-- 在组件中:this.liveAnnouncer.announce('已添加到购物车'); -->

常见的 ARIA 错误

错误 为什么错了 修复方法
使用 <div role="button"> 但未实现键盘事件 div 不会接收键盘事件 使用 <button> 或添加 tabindex="0" + onkeydown
在可聚焦元素上使用 aria-hidden="true" 屏幕阅读器跳过它,但键盘仍可聚焦 同时从 Tab 顺序中移除:tabindex="-1"
aria-label 覆盖了可见文本 对有视力的屏幕阅读器用户造成困惑 使用 aria-labelledby 指向可见文本
在语义 HTML 上添加冗余的 ARIA <nav role="navigation"> 是多余的 去掉 role——<nav> 已隐含该角色
在已有内容的容器上使用 aria-live 初始内容会在加载时被播报 aria-live 加在空容器上,之后再注入内容
切换开关缺少 aria-expanded 屏幕阅读器无法判断区域是否展开 添加 aria-expanded="true/false"