键盘导航
键盘导航是无障碍的核心能力之一,许多用户依赖键盘来浏览和操作网页——包括使用屏幕阅读器的视障用户、无法使用鼠标的运动障碍用户,以及偏好键盘操作的高级用户。本节将详细介绍 Tab 键导航顺序、常见键盘交互模式(Enter/Space/Escape 等),以及如何为自定义组件实现完整的键盘支持。
前置知识
阅读本节前,建议先了解:焦点管理
为什么键盘导航重要
键盘导航是 Web 无障碍的基石:
- WCAG 2.1 准则 2.1.1(A 级):所有功能性内容必须可通过键盘操作
- 屏幕阅读器用户:完全依赖键盘导航页面
- 运动障碍用户:无法使用鼠标或触摸屏
- 高级用户:偏好键盘快捷键提升效率
Tab 键导航
默认 Tab 顺序
浏览器默认按照 DOM 顺序进行 Tab 导航。只有特定的"可聚焦"元素会参与 Tab 导航:
| 元素类型 | 默认可聚焦 | 说明 |
|---|---|---|
<a href="..."> | 是 | 链接(有 href 时) |
<button> | 是 | 按钮 |
<input> | 是 | 输入框(非 hidden/disabled) |
<select> | 是 | 下拉选择框 |
<textarea> | 是 | 文本域 |
<area>(带 href) | 是 | 图片映射区域 |
<iframe> | 是(聚焦到 iframe 内) | 内嵌框架 |
<details> / <summary> | 是 | 可展开内容 |
[contenteditable] | 是 | 可编辑元素 |
<audio> / <video> | 是 | 媒体控件 |
<div> / <span> | 否 | 需要添加 tabindex |
tabindex 控制焦点
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Tab 顺序示例</title>
</head>
<body>
<!-- 按 DOM 顺序 Tab 导航 -->
<nav>
<a href="#home">首页</a> <!-- Tab 1 -->
<a href="#products">产品</a> <!-- Tab 2 -->
<a href="#about">关于</a> <!-- Tab 3 -->
</nav>
<form>
<input type="text" placeholder="搜索" /> <!-- Tab 4 -->
<button type="submit">搜索</button> <!-- Tab 5 -->
</form>
<!-- 使用 tabindex="0" 使非交互元素可聚焦 -->
<div tabindex="0" role="button" aria-label="收藏">
♡
</div> <!-- Tab 6 -->
</body>
</html>常用键盘快捷键
以下是浏览器和辅助技术常用的键盘快捷键:
| 按键 | 功能 | 说明 |
|---|---|---|
Tab | 移动到下一个可聚焦元素 | 按住 Shift 为反方向 |
Enter | 激活链接和按钮 | 对于单行输入框也触发表单提交 |
Space | 激活按钮、切换复选框 | 对于输入框输入空格 |
Escape | 关闭弹出层、取消操作 | 取消当前操作 |
Arrow keys | 在选项间移动 | 用于列表框、菜单、选项卡等 |
Home / End | 跳到首/末选项 | 用于列表和输入框 |
Page Up / Page Down | 翻页 | 用于长列表和文本区域 |
各组件类型的键盘交互规范
按钮(Button)
html
<!-- 原生按钮自动支持键盘交互 -->
<button onclick="handleClick()">点击我</button>
<!-- 自定义按钮需要手动添加键盘支持 -->
<div role="button" tabindex="0"
onclick="handleClick()"
onkeydown="handleButtonKeydown(event)">
点击我
</div>
<script>
function handleButtonKeydown(event) {
// 按钮支持 Enter 和 Space 激活
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); // Space 会触发页面滚动,需要阻止
handleClick();
}
}
</script>| 按键 | 行为 |
|---|---|
Enter | 激活按钮 |
Space | 激活按钮(需 preventDefault 防止滚动) |
链接(Link)
原生链接已经支持完整的键盘交互:
| 按键 | 行为 |
|---|---|
Enter | 跟随链接 |
Tab | 移动到下一个链接 |
复选框(Checkbox)
html
<!-- 原生复选框自动支持键盘 -->
<label>
<input type="checkbox" id="agree" />
我已阅读并同意服务条款
</label>
<!-- 自定义复选框 -->
<div role="checkbox" aria-checked="false" tabindex="0"
onclick="toggle(this)"
onkeydown="handleCheckboxKeydown(event)">
我已阅读并同意服务条款
</div>
<script>
function handleCheckboxKeydown(event) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
toggle(event.currentTarget);
}
}
function toggle(el) {
const isChecked = el.getAttribute('aria-checked') === 'true';
el.setAttribute('aria-checked', String(!isChecked));
}
</script>| 按键 | 行为 |
|---|---|
Space | 切换选中状态 |
单选按钮组(Radio Group)
html
<div role="radiogroup" aria-label="配送方式">
<div role="radio" tabindex="0" aria-checked="true" onkeydown="handleRadioKeydown(event)">
标准配送(3-5天)
</div>
<div role="radio" tabindex="-1" aria-checked="false" onkeydown="handleRadioKeydown(event)">
快速配送(1-2天)
</div>
<div role="radio" tabindex="-1" aria-checked="false" onkeydown="handleRadioKeydown(event)">
当日达
</div>
</div>
<script>
function handleRadioKeydown(event) {
const radios = Array.from(event.currentTarget.parentElement.querySelectorAll('[role="radio"]'));
const currentIndex = radios.indexOf(event.currentTarget);
let nextIndex;
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
nextIndex = (currentIndex + 1) % radios.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
nextIndex = (currentIndex - 1 + radios.length) % radios.length;
break;
case ' ':
// Space 选中当前选项
event.preventDefault();
selectRadio(radios, currentIndex);
return;
default:
return;
}
event.preventDefault();
// 移动焦点和选中状态
radios.forEach(r => {
r.setAttribute('tabindex', '-1');
r.setAttribute('aria-checked', 'false');
});
radios[nextIndex].setAttribute('tabindex', '0');
radios[nextIndex].setAttribute('aria-checked', 'true');
radios[nextIndex].focus();
}
function selectRadio(radios, index) {
radios.forEach(r => r.setAttribute('aria-checked', 'false'));
radios[index].setAttribute('aria-checked', 'true');
}
</script>| 按键 | 行为 |
|---|---|
Arrow Up / Arrow Left | 移动到上一个选项并选中 |
Arrow Down / Arrow Right | 移动到下一个选项并选中 |
Space | 选中当前选项 |
Tab | 移出单选组(只聚焦到选中项) |
下拉菜单(Dropdown Menu)
html
<nav>
<button aria-haspopup="true" aria-expanded="false"
onclick="toggleMenu(this)"
onkeydown="handleMenuTriggerKeydown(event)">
菜单
</button>
<ul role="menu" hidden>
<li role="menuitem"><a href="#">新建</a></li>
<li role="menuitem"><a href="#">打开</a></li>
<li role="menuitem"><a href="#">保存</a></li>
</ul>
</nav>
<script>
function handleMenuTriggerKeydown(event) {
if (event.key === 'ArrowDown') {
event.preventDefault();
// 打开菜单并聚焦第一个菜单项
const menu = event.currentTarget.nextElementSibling;
menu.hidden = false;
event.currentTarget.setAttribute('aria-expanded', 'true');
menu.querySelector('[role="menuitem"]').focus();
} else if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleMenu(event.currentTarget);
}
}
</script>| 按键(触发器) | 行为 |
|---|---|
Enter / Space | 打开/关闭菜单 |
Arrow Down | 打开菜单并聚焦第一项 |
| 按键(菜单内) | 行为 |
|---|---|
Arrow Up / Arrow Down | 在菜单项间移动 |
Enter | 激活当前菜单项 |
Escape | 关闭菜单,焦点回到触发器 |
Tab | 关闭菜单,焦点移到下一元素 |
选项卡(Tabs)
html
<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>
<button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3" tabindex="-1">
评价
</button>
</div>
<!-- 选项卡内的键盘交互 -->
<script>
const tablist = document.querySelector('[role="tablist"]');
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
tablist.addEventListener('keydown', function(e) {
const currentIndex = tabs.indexOf(e.target);
let nextIndex;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
nextIndex = (currentIndex + 1) % tabs.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
// 更新 tabindex 和 aria-selected
tabs.forEach(tab => {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('aria-selected', 'false');
});
tabs[nextIndex].setAttribute('tabindex', '0');
tabs[nextIndex].setAttribute('aria-selected', 'true');
tabs[nextIndex].focus();
// 激活对应面板
activatePanel(tabs[nextIndex]);
});
</script>| 按键 | 行为 |
|---|---|
Arrow Right / Arrow Down | 聚焦下一个选项卡 |
Arrow Left / Arrow Up | 聚焦上一个选项卡 |
Home | 聚焦第一个选项卡 |
End | 聚焦最后一个选项卡 |
Tab | 移出选项卡组 |
键盘事件处理注意事项
使用 key 而非 keyCode
javascript
// 推荐:使用 event.key
element.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
}
});
// 不推荐:使用过时的 keyCode
element.addEventListener('keydown', function(event) {
if (event.keyCode === 27) { // 27 是 Escape 的 keyCode
closeModal();
}
});阻止默认行为
某些按键有浏览器默认行为,需要根据情况阻止:
javascript
// Space 在按钮中需要阻止默认的页面滚动
element.addEventListener('keydown', function(event) {
if (event.key === ' ') {
event.preventDefault(); // 阻止页面滚动
activateButton();
}
});
// Arrow keys 在自定义列表中需要阻止页面滚动
element.addEventListener('keydown', function(event) {
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
event.preventDefault(); // 阻止页面滚动
navigateList(event.key);
}
});实战示例:无障碍手风琴组件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>无障碍手风琴组件</title>
<style>
.accordion-item { border: 1px solid #ddd; margin-bottom: 0.25rem; }
.accordion-header {
width: 100%;
padding: 1rem;
background: #f5f5f5;
border: none;
cursor: pointer;
text-align: left;
font-size: 1rem;
font-weight: bold;
}
.accordion-panel {
padding: 1rem;
display: none;
}
.accordion-panel[aria-hidden="false"] { display: block; }
.accordion-header:focus-visible {
outline: 3px solid #0066cc;
outline-offset: -3px;
}
</style>
</head>
<body>
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header"
id="header-1"
aria-expanded="true"
aria-controls="panel-1">
什么是 HTML5?
</button>
<div class="accordion-panel"
id="panel-1"
role="region"
aria-labelledby="header-1"
aria-hidden="false">
<p>HTML5 是超文本标记语言的第五个主要版本,引入了许多新特性,包括语义化标签、多媒体支持、Canvas 绘图、地理定位等。</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header"
id="header-2"
aria-expanded="false"
aria-controls="panel-2">
HTML5 新增了哪些语义化标签?
</button>
<div class="accordion-panel"
id="panel-2"
role="region"
aria-labelledby="header-2"
aria-hidden="true">
<p>HTML5 新增的语义化标签包括:header、footer、nav、main、article、section、aside、figure、figcaption、details、summary、dialog 等。</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header"
id="header-3"
aria-expanded="false"
aria-controls="panel-3">
如何开始学习 HTML5?
</button>
<div class="accordion-panel"
id="panel-3"
role="region"
aria-labelledby="header-3"
aria-hidden="true">
<p>学习 HTML5 的最佳方式是从基础开始,逐步掌握语义化标签、表单控件、多媒体元素、Canvas/SVG、Web API 等。</p>
</div>
</div>
</div>
<script>
// 为所有手风琴按钮添加键盘事件
document.querySelectorAll('.accordion-header').forEach(button => {
button.addEventListener('click', toggleAccordion);
button.addEventListener('keydown', handleAccordionKeydown);
});
function toggleAccordion(button) {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
const panelId = button.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
button.setAttribute('aria-expanded', String(!isExpanded));
panel.setAttribute('aria-hidden', String(isExpanded));
}
function handleAccordionKeydown(event) {
const buttons = Array.from(document.querySelectorAll('.accordion-header'));
const currentIndex = buttons.indexOf(event.currentTarget);
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
buttons[(currentIndex + 1) % buttons.length].focus();
break;
case 'ArrowUp':
event.preventDefault();
buttons[(currentIndex - 1 + buttons.length) % buttons.length].focus();
break;
case 'Home':
event.preventDefault();
buttons[0].focus();
break;
case 'End':
event.preventDefault();
buttons[buttons.length - 1].focus();
break;
}
}
</script>
</body>
</html>注意事项
- 不要移除 outline:
* { outline: none }会导致键盘用户无法看到焦点位置 - 自定义组件必须实现键盘支持:如果用
<div>替代<button>,必须手动添加键盘事件 - 避免键盘陷阱:确保用户可以 Tab 进出所有组件
- 测试完整的键盘流程:只用键盘操作完成所有页面功能
最佳实践
- 优先使用原生 HTML 元素,它们自带键盘交互支持
- 为自定义组件遵循 APG(ARIA Authoring Practices)的键盘交互模式
- 使用
event.key而非event.keyCode - 适当阻止默认行为(如 Space 的页面滚动)
- 提供键盘快捷键的同时,不影响屏幕阅读器的快捷键
- 在帮助页面或设置页面中列出所有自定义快捷键
下一节
继续学习:跳过导航链接