Skip to content

焦点管理

焦点管理是 Web 无障碍中至关重要的主题。键盘用户依赖焦点在页面元素间导航,而屏幕阅读器用户则依赖焦点来确定当前正在交互的元素。本节将详细介绍 JavaScript 焦点操作方法(focus()blur())、CSS 焦点伪类(focus-visiblefocus-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" 或手动管理

注意事项

  1. 不要移除焦点指示器outline: none 会使键盘用户无法看到当前位置
  2. 避免滥用 focus():不要在页面加载时自动将焦点移到非预期位置
  3. 焦点陷阱必须可逆:确保用户可以通过 Escape 键退出
  4. 测试键盘导航:确保所有交互功能都可以通过 Tab 键完成
  5. 考虑移动设备:蓝牙键盘用户同样需要焦点支持

最佳实践

  • 使用 :focus-visible 代替 :focus 来显示焦点指示器
  • 使用 focus-within 为表单组和搜索容器提供视觉反馈
  • 为模态对话框实现完整的焦点陷阱
  • 使用 tabindex="-1" 为非交互元素添加程序化焦点能力
  • 避免在页面加载时使用 autofocus(除非有充分理由)

下一节

继续学习:键盘导航

参考链接