本文由 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 的操作要求。
可访问键盘导航的三条规则:
- 每个可键盘导航的组件都必须纳入 tab 顺序——通过
tabindex="0"或role - 焦点状态必须可见——永远不要在没有替代可见焦点指示器的情况下使用
outline: none - 屏幕阅读器必须了解组件状态——使用
aria-pressed、aria-expanded、aria-selected或aria-activedescendant
TanStack Hotkeys:绑定层
TanStack Hotkeys 提供事件绑定,处理 keydown/keyup 绑定、作用域管理,以及原始事件监听器中繁琐的修饰键处理。
npm install @tanstack/react-hotkeys
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 的作用域解决了这个问题。当一个作用域"激活"时,它的快捷键触发;当作用域"未激活"时,快捷键被抑制。
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 作用域中注册的快捷键除外。
// 全局快捷键在所有作用域中都触发
useHotkeys(
'command+n',
() => createNewDocument(),
{ scopes: ['global'], enableOnFormTags: true }
)
enableOnFormTags: true 使快捷键即使在表单输入框内也能触发——适用于"新建文档"这类无论从哪里都应该工作的全局快捷键。
Roving Tabindex:复杂组件的 ARIA 模式
对于工具栏或菜单这类复杂组件,tab 顺序不是线性的。组件整体获得焦点(一个 tab 停靠点),方向键在组件内部导航。这就是 roving tabindex 模式。
TanStack Hotkeys 通过作用域管理支持这种模式:
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
useHotkeysScopeAPI 在 1.0 之前可能会改变
对于今天就需要稳定性的生产应用,可以风险自担地使用 @tanstack/react-hotkeys,或者使用更稳定的 react-hotkeys 库配合手动作用域管理。
TanStack Hotkeys 是 TanStack Ship 中的可选依赖。 查看完整功能列表以及键盘无障碍功能如何融入 WCAG 2.1 合规模式。