Skip to content

键盘导航

键盘导航是无障碍的核心能力之一,许多用户依赖键盘来浏览和操作网页——包括使用屏幕阅读器的视障用户、无法使用鼠标的运动障碍用户,以及偏好键盘操作的高级用户。本节将详细介绍 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 防止滚动)

原生链接已经支持完整的键盘交互:

按键行为
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>

注意事项

  1. 不要移除 outline* { outline: none } 会导致键盘用户无法看到焦点位置
  2. 自定义组件必须实现键盘支持:如果用 <div> 替代 <button>,必须手动添加键盘事件
  3. 避免键盘陷阱:确保用户可以 Tab 进出所有组件
  4. 测试完整的键盘流程:只用键盘操作完成所有页面功能

最佳实践

  • 优先使用原生 HTML 元素,它们自带键盘交互支持
  • 为自定义组件遵循 APG(ARIA Authoring Practices)的键盘交互模式
  • 使用 event.key 而非 event.keyCode
  • 适当阻止默认行为(如 Space 的页面滚动)
  • 提供键盘快捷键的同时,不影响屏幕阅读器的快捷键
  • 在帮助页面或设置页面中列出所有自定义快捷键

下一节

继续学习:跳过导航链接

参考链接