TanStack Hotkeys: React 键盘快捷键快速构建指南

用 TanStack Hotkeys 给 React 应用加键盘快捷键,不破坏屏幕阅读器、不与浏览器键冲突。支持作用域隔离和焦点陷阱,零配置开箱即用。

Huifer
Huifer
September 18, 20268 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,TanStack Ship 的独立开发者和维护者。 2025 年,我为一个合规管理后台实现了键盘导航功能——那是一个面向政府相关 SaaS 的项目,需要满足 WCAG 2.1 AA 标准。最难的部分不是键盘快捷键本身,而是让它们在不破坏屏幕阅读器、不与浏览器快捷键冲突、不被 React 组件模型的焦点管理拖后腿的情况下正常工作。我的教训是:<div> 上的 onKeyDown 不是键盘导航——它是一种排除了 20% 用户的"快捷方式"。这篇指南就是我的经验总结。

验证来源:TanStack Hotkeys on GitHub · WAI-ARIA Keyboard Navigation · MDN Keyboard Event Handling Last updated: 2026-09-18 · Changelog

TL;DR: React 中的键盘快捷键需要三件事:正确的 ARIA 组件模式(可访问性)、作用域隔离(防止在输入框中误触发)以及浏览器快捷键冲突管理。TanStack Hotkeys(@tanstack/react-hotkeys,alpha)提供了绑定层;可访问性模式需要你自己负责。


可访问性基础

在写任何键盘快捷键之前,先理解这一点:键盘导航不是一种快捷功能。它是可访问性要求,面向无法使用鼠标的用户。 包括运动障碍用户、偏好纯键盘工作流的资深用户,以及在 IDE 中使用 Vim 键绑定的 20% 开发者。

带有 onKeyDown 处理程序的 <div> 可以通过 WCAG 2.1 的技术要求。但如果焦点管理没有传达给辅助技术,它就满足不了 WCAG 2.1 的操作要求。

可访问键盘导航的三条规则:

  1. 每个可键盘导航的组件都必须纳入 tab 顺序——通过 tabindex="0" 或 role
  2. 焦点状态必须可见——永远不要在没有替代可见焦点指示器的情况下使用 outline: none
  3. 屏幕阅读器必须了解组件状态——使用 aria-pressed、aria-expanded、aria-selected 或 aria-activedescendant

TanStack Hotkeys:绑定层

TanStack Hotkeys 提供事件绑定,处理 keydown/keyup 绑定、作用域管理,以及原始事件监听器中繁琐的修饰键处理。

bash
npm install @tanstack/react-hotkeys
tsx
import { HotkeysProvider, useHotkeys } from '@tanstack/react-hotkeys'

function App() {
  return (
    <HotkeysProvider>
      <Editor />
    </HotkeysProvider>
  )
}

function Editor() {
  useHotkeys(
    'command+k', // macOS
    (event) => {
      event.preventDefault()
      openCommandPalette()
    },
    { scopes: ['editor'] }
  )

  useHotkeys(
    'command+b',
    (event) => {
      event.preventDefault()
      toggleBold()
    },
    { scopes: ['editor'] }
  )

  return <div>Editor content</div>
}

useHotkeys 在一个作用域内绑定键盘事件。作用域对于作用域隔离至关重要:当用户在文本输入框中时,编辑器的 command+k 快捷键不应触发。


作用域隔离:让快捷键在真实应用中正常工作的模式

键盘快捷键实现中最常见的 bug:快捷键在文本输入框内触发,破坏了用户的输入能力。

TanStack Hotkeys 的作用域解决了这个问题。当一个作用域"激活"时,它的快捷键触发;当作用域"未激活"时,快捷键被抑制。

tsx
import { HotkeysProvider, useHotkeys, useHotkeysScope } from '@tanstack/react-hotkeys'

function Editor() {
  useHotkeys(
    'command+k',
    () => openCommandPalette(),
    { scopes: ['editor'] }
  )

  return (
    <div>
      <RichTextEditor
        onFocus={() => useHotkeysScope('editor')}
        onBlur={() => useHotkeysScope('global')}
      />
    </div>
  )
}

useHotkeysScope hook 根据焦点激活或停用一个作用域。当用户在富文本编辑器中时,editor 作用域激活,快捷键触发。当用户 Tab 到编辑器外的文本输入框时,作用域停用,快捷键被抑制——global 作用域中注册的快捷键除外。

tsx
// 全局快捷键在所有作用域中都触发
useHotkeys(
  'command+n',
  () => createNewDocument(),
  { scopes: ['global'], enableOnFormTags: true }
)

enableOnFormTags: true 使快捷键即使在表单输入框内也能触发——适用于"新建文档"这类无论从哪里都应该工作的全局快捷键。


Roving Tabindex:复杂组件的 ARIA 模式

对于工具栏或菜单这类复杂组件,tab 顺序不是线性的。组件整体获得焦点(一个 tab 停靠点),方向键在组件内部导航。这就是 roving tabindex 模式。

TanStack Hotkeys 通过作用域管理支持这种模式:

tsx
function Toolbar() {
  const items = ['bold', 'italic', 'underline', 'link']
  const [activeIndex, setActiveIndex] = useState(-1)

  useHotkeys(
    'left',
    () => {
      setActiveIndex(i => Math.max(0, i - 1))
    },
    { scopes: ['toolbar'] }
  )

  useHotkeys(
    'right',
    () => {
      setActiveIndex(i => Math.min(items.length - 1, i + 1))
    },
    { scopes: ['toolbar'] }
  )

  useHotkeys(
    'enter',
    () => {
      if (activeIndex >= 0) {
        activateItem(items[activeIndex])
      }
    },
    { scopes: ['toolbar'] }
  )

  return (
    <div
      role="toolbar"
      tabIndex={0}
      onFocus={() => {
        setActiveIndex(-1) // 初始时没有项目激活
        useHotkeysScope('toolbar')
      }}
      onBlur={() => useHotkeysScope('global')}
    >
      {items.map((item, i) => (
        <button
          key={item}
          aria-pressed={activeIndex === i}
          tabIndex={activeIndex === i ? 0 : -1}
          onClick={() => activateItem(item)}
        >
          {item}
        </button>
      ))}
    </div>
  )
}

Roving tabindex 模式:只有当前激活的项目有 tabIndex={0},其他所有项目都是 tabIndex={-1}。用户按 Tab 时,只有激活的项目在 tab 顺序中。方向键移动 activeIndex 并触发重新渲染——之前激活的项目变成 tabIndex={-1},新的激活项目变成 tabIndex={0}。屏幕阅读器通过 aria-pressed 知道哪个项目是激活状态。


浏览器快捷键冲突

三个浏览器快捷键无法被覆盖:

  • Command/Ctrl+W — 关闭标签页
  • Command/Ctrl+T — 新建标签页
  • Command/Ctrl+R — 刷新
  • Command/Ctrl+Shift+T — 重新打开关闭的标签页
  • F5 / Ctrl+R — 刷新

TanStack Hotkeys 对匹配的快捷键调用 event.preventDefault()。对于上面的快捷键,preventDefault() 会被浏览器静默忽略。如果你的应用需要处理这些快捷键,必须在应用层面做——但要知道浏览器行为优先级更高。

我在生产环境中遇到的最常见冲突:用 Escape 关闭模态框。如果用户安装了为自己的 UI 拦截 Escape 的浏览器扩展,你的模态框就不会关闭。解决方法是在模态框元素上也监听 onKeyDown——元素级事件在全局监听器之前触发。


生产检查清单

  • 每个键盘快捷键都有可访问的替代方案——一个做同样事情的按钮、菜单项或 UI 控件
  • 焦点状态可见——使用在所有背景上都可见的 CSS :focus-visible 轮廓
  • 快捷键不会在意图之外的文本输入框中触发——使用作用域隔离
  • 复杂组件使用 roving tabindex——工具栏、菜单、树形控件、listbox 的 ARIA 模式
  • 快捷键有文档——在应用中包含一个键盘快捷键面板(按 ? 打开)
  • 屏幕阅读器测试——发布前用 VoiceOver(macOS)或 NVDA(Windows)测试应用
  • 无浏览器快捷键冲突——测试 Escape 能否正确关闭模态框,不要仅依赖 preventDefault()

相关阅读: Tanstack Form Complete Guide · Tanstack Pacer Complete Guide 2026 · Tanstack Virtual Complete Guide 2026

何时等待 TanStack Hotkeys

TanStack Hotkeys 处于 alpha 阶段。API 还不稳定。在已发布产品中使用它之前:

  • 在 package.json 中锁定到精确版本
  • 关注 TanStack Hotkeys GitHub 的 breaking changes
  • useHotkeysScope API 在 1.0 之前可能会改变

对于今天就需要稳定性的生产应用,可以风险自担地使用 @tanstack/react-hotkeys,或者使用更稳定的 react-hotkeys 库配合手动作用域管理。


TanStack Hotkeys 是 TanStack Ship 中的可选依赖。 查看完整功能列表以及键盘无障碍功能如何融入 WCAG 2.1 合规模式。