焦点管理
焦点管理是 Web 无障碍中至关重要的主题。键盘用户依赖焦点在页面元素间导航,而屏幕阅读器用户则依赖焦点来确定当前正在交互的元素。本节将详细介绍 JavaScript 焦点操作方法(focus()、blur())、CSS 焦点伪类(focus-visible、focus-within)、焦点陷阱技术,以及模态对话框的焦点管理最佳实践。
前置知识
阅读本节前,建议先了解:ARIA Live Regions
什么是焦点
焦点(Focus)是浏览器中指示当前活跃交互元素的一种机制。获得焦点的元素可以接收键盘输入,并通过焦点指示器(通常是轮廓线)在视觉上高亮显示。
html
<!-- Tab 键在不同元素间移动焦点 -->
<form>
<label for="name">姓名</label>
<input type="text" id="name" /> <!-- 按一次 Tab 聚焦 -->
<label for="email">邮箱</label>
<input type="email" id="email" /> <!-- 按两次 Tab 聚焦 -->
<button type="submit">提交</button> <!-- 按三次 Tab 聚焦 -->
</form>focus() 和 blur() 方法
element.focus()
focus() 方法用于将焦点设置到指定元素上:
html
<!-- 点击按钮后聚焦到搜索框 -->
<button onclick="document.getElementById('search').focus()">
聚焦搜索框
</button>
<input type="search" id="search" />element.blur()
blur() 方法用于从当前元素移除焦点:
html
<input type="text" id="input-field" />
<button onclick="document.getElementById('input-field').blur()">
移除焦点
</button>慎用 blur()
一般情况下不应主动调用 blur(),因为这会中断用户的操作流程。如果目的是阻止某个操作,应考虑使用 preventDefault()。
focus() 的参数选项
javascript
// preventScroll: 聚焦时不滚动页面到元素位置(默认 false)
element.focus({
preventScroll: true
});
// 适用于:聚焦到屏幕外但需要聚焦的元素
document.getElementById('modal-first-focusable').focus({
preventScroll: false // 默认行为:滚动到元素
});CSS 焦点伪类
:focus
当元素获得焦点时匹配:
css
/* 所有获得焦点的元素 */
:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}:focus-visible
仅在键盘导航触发焦点时显示焦点样式,鼠标点击时不显示:
css
/* 键盘焦点:显示轮廓 */
:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* 鼠标点击时不显示轮廓 */
:focus:not(:focus-visible) {
outline: none;
}html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>focus-visible 示例</title>
<style>
.btn {
padding: 0.5rem 1.5rem;
border: 2px solid #0066cc;
background: #0066cc;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn:focus {
outline: none;
}
.btn:focus-visible {
outline: 3px solid #ff6600;
outline-offset: 3px;
box-shadow: 0 0 0 3px rgba(255, 102, 0, 0.3);
}
.input-field:focus {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
.input-field:focus-visible {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
</style>
</head>
<body>
<h1>焦点样式示例</h1>
<p>使用 Tab 键导航可以看到焦点轮廓,使用鼠标点击不会显示轮廓。</p>
<button class="btn">主要按钮</button>
<input type="text" class="input-field" placeholder="输入框" />
<button class="btn">次要按钮</button>
</body>
</html>:focus-within
当元素自身或其任意子元素获得焦点时匹配:
css
/* 表单组中任一子元素获得焦点时高亮整个表单组 */
.form-group {
border: 2px solid #ddd;
padding: 1rem;
border-radius: 8px;
transition: border-color 0.2s;
}
.form-group:focus-within {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15);
}html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>focus-within 示例</title>
<style>
.form-group {
border: 2px solid #e0e0e0;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
transition: all 0.2s ease;
}
.form-group:focus-within {
border-color: #0066cc;
background: #f0f7ff;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #0066cc;
}
</style>
</head>
<body>
<form>
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" />
</div>
</form>
</body>
</html>Tab 顺序控制
tabindex 属性
| 值 | 行为 |
|---|---|
| 不设置 | 默认行为,只有可交互元素可聚焦 |
tabindex="0" | 元素可聚焦,按 DOM 顺序排列 |
tabindex="-1" | 元素可通过 JavaScript 聚焦,但不在 Tab 顺序中 |
tabindex="1" 及以上 | 按数值从小到大排序(不推荐) |
html
<!-- 非交互元素添加到 Tab 顺序 -->
<div tabindex="0" role="button" aria-label="播放视频">
▶ 播放
</div>
<!-- 通过 JavaScript 聚焦但不在 Tab 顺序中 -->
<div tabindex="-1" id="modal-focus-trap-start">
<!-- 用于焦点陷阱 -->
</div>避免 tabindex > 0
正数 tabindex 会打乱自然的 Tab 顺序,增加维护难度。应使用 tabindex="0" 并调整 DOM 顺序。
焦点陷阱(Focus Trap)
焦点陷阱用于将焦点限制在某个容器内(如模态对话框),防止用户 Tab 到容器外部的元素。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>焦点陷阱示例</title>
<style>
body { font-family: sans-serif; padding: 2rem; }
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
.modal h2 { margin-top: 0; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1.5rem;
}
.btn {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary { background: #0066cc; color: white; }
.btn-secondary { background: #eee; color: #333; }
:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
</style>
</head>
<body>
<h1>焦点陷阱演示</h1>
<button onclick="openModal()">打开对话框</button>
<div class="modal-overlay" id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
<div class="modal">
<h2 id="modal-title">确认操作</h2>
<p>确定要删除这个文件吗?此操作不可撤销。</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="confirmDelete()">确认删除</button>
</div>
</div>
</div>
<script>
let lastFocusedElement = null;
function openModal() {
const modal = document.getElementById('modal');
lastFocusedElement = document.activeElement;
modal.hidden = false;
// 聚焦到模态对话框中第一个可聚焦元素
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const firstFocusable = modal.querySelector(focusableSelectors);
if (firstFocusable) {
firstFocusable.focus();
}
modal.addEventListener('keydown', handleTrapKeydown);
}
function closeModal() {
const modal = document.getElementById('modal');
modal.hidden = true;
modal.removeEventListener('keydown', handleTrapKeydown);
// 恢复之前的焦点
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
function confirmDelete() {
closeModal();
}
function handleTrapKeydown(e) {
const modal = document.getElementById('modal');
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = Array.from(modal.querySelectorAll(focusableSelectors));
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.key === 'Escape') {
e.preventDefault();
closeModal();
return;
}
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
}
</script>
</body>
</html>模态对话框焦点管理规范
| 步骤 | 操作 | 说明 |
|---|---|---|
| 打开时 | 保存当前焦点元素 | 记录 document.activeElement |
| 打开时 | 将焦点移到对话框内 | 聚焦第一个交互元素或对话框本身 |
| 打开时 | 实现焦点陷阱 | Tab/Shift+Tab 在对话框内循环 |
| 关闭时 | 恢复之前保存的焦点 | 恢复到打开前的元素 |
| 打开时 | 阻止背景交互 | 使用 aria-modal="true" 或手动管理 |
注意事项
- 不要移除焦点指示器:
outline: none会使键盘用户无法看到当前位置 - 避免滥用 focus():不要在页面加载时自动将焦点移到非预期位置
- 焦点陷阱必须可逆:确保用户可以通过 Escape 键退出
- 测试键盘导航:确保所有交互功能都可以通过 Tab 键完成
- 考虑移动设备:蓝牙键盘用户同样需要焦点支持
最佳实践
- 使用
:focus-visible代替:focus来显示焦点指示器 - 使用
focus-within为表单组和搜索容器提供视觉反馈 - 为模态对话框实现完整的焦点陷阱
- 使用
tabindex="-1"为非交互元素添加程序化焦点能力 - 避免在页面加载时使用
autofocus(除非有充分理由)
下一节
继续学习:键盘导航