6.1 KiB
6.1 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| aria-patterns-reference | ARIA 模式与键盘交互参考 |
|
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" |