TanStack Hotkeys — Knowledge Reference
Status: Alpha (as of early 2026). Actively developed; API surface may evolve. Package:
@tanstack/react-hotkeys(also available for Preact, Solid, Svelte, Angular, Vue, Lit, and vanilla JS) Docs: https://tanstack.com/hotkeys/latest/docs/overview
What This Library Is
TanStack Hotkeys is a type-safe keyboard shortcuts library for the web. It provides template-string bindings (e.g. 'Mod+Shift+S'), a cross-platform Mod key abstraction, a singleton HotkeyManager under the hood, and utilities for building cheatsheet UIs — all while staying SSR-friendly.
It is not a general-purpose key-event listener. It is purpose-built for registering, managing, displaying, and letting users customize keyboard shortcuts in applications.\
Core Concepts
The Mod Modifier
The Mod modifier is the library's main cross-platform abstraction. It maps to Command (⌘) on macOS and Control (Ctrl) on Windows/Linux. Using Mod instead of hard-coding Meta or Control means a single hotkey definition works everywhere.
useHotkey('Mod+S', () => save())
// Mac → ⌘S | Windows/Linux → Ctrl+SHotkey Definitions — Two Formats
Hotkeys can be defined as either a template string or a raw object:
// String format (most common)
useHotkey('Mod+Shift+S', handler)
useHotkey('Escape', handler)
// Object format (full control)
useHotkey({ key: 'S', mod: true, shift: true }, handler)TypeScript provides autocomplete and validation for both formats — invalid key combinations produce type errors at compile time.
Singleton HotkeyManager
All registrations go through a single centralized HotkeyManager instance. This enables conflict detection, automatic input filtering, per-target listeners, and powers the devtools. You don't interact with it directly in React — the hooks abstract it away.
API Surface — React Hooks
1. useHotkey — The Primary Hook
Registers a single keyboard shortcut. Automatic lifecycle management (registers on mount, unregisters on unmount). Stale-closure prevention — callbacks always have access to latest React state.
import { useHotkey } from '@tanstack/react-hotkeys'
useHotkey('Mod+S', (event, context) => {
console.log(context.hotkey) // 'Mod+S'
console.log(context.parsedHotkey) // { key: 'S', mod: true, ... }
saveDocument()
})Full options with defaults:
useHotkey('Mod+S', callback, {
enabled: true, // Toggle on/off without unregistering
preventDefault: true, // Calls event.preventDefault()
stopPropagation: true, // Calls event.stopPropagation()
eventType: 'keydown', // Or 'keyup' (e.g. for push-to-talk release)
requireReset: false, // If true, must release all keys before re-triggering
ignoreInputs: undefined, // Smart default (see below)
target: document, // DOM element or React ref to scope the hotkey
platform: undefined, // Auto-detected; override with 'mac' | 'windows' | 'linux'
conflictBehavior: 'warn', // What happens when two hotkeys collide
meta: {}, // Arbitrary metadata (visible in devtools/registrations)
})2. useHotkeys (plural) — Batch Registration
Register multiple hotkeys at once, or a dynamic/variable-length list:
import { useHotkeys } from '@tanstack/react-hotkeys'
useHotkeys([
{ hotkey: 'Mod+S', callback: () => save() },
{ hotkey: 'Mod+Z', callback: () => undo() },
{ hotkey: 'Escape', callback: () => close() },
])Useful when hotkey bindings are stored in state (e.g. user-customizable shortcuts).
3. useHotkeySequence — Multi-Step Shortcuts
Vim-style key sequences where keys are pressed one after another (not simultaneously):
import { useHotkeySequence } from '@tanstack/react-hotkeys'
useHotkeySequence(['G', 'G'], () => scrollToTop()) // press g then g
useHotkeySequence(['G', 'Shift+G'], () => scrollToBottom()) // press g then GSequences have a configurable timeout between steps (default can be set via HotkeysProvider).
4. useKeyHold — Is a Specific Key Held?
Returns a boolean indicating whether a specific key is currently pressed:
import { useKeyHold } from '@tanstack/react-hotkeys'
const isShiftHeld = useKeyHold('Shift')
// true while Shift is physically held downUse case: Showing alternate tooltips, switching tool modes, enabling "power user" overlays while a modifier is held.
5. useHeldKeys / useHeldKeyCodes — All Currently Held Keys
Returns an array of all keys currently pressed:
import { useHeldKeys } from '@tanstack/react-hotkeys'
const heldKeys = useHeldKeys()
// e.g. ['Shift', 'A'] while both are held6. useHotkeyRecorder — Let Users Customize Shortcuts
Provides all the logic for building a "press keys to record shortcut" UI:
import { useHotkeyRecorder, formatForDisplay } from '@tanstack/react-hotkeys'
const {
isRecording, // boolean — is the recorder listening?
recordedHotkey, // Hotkey | null — the captured combination
startRecording, // () => void
stopRecording, // () => void
cancelRecording, // () => void
} = useHotkeyRecorder({
onRecord: (hotkey) => console.log('Captured:', hotkey),
onCancel: () => console.log('Cancelled'),
onClear: () => console.log('Cleared'),
})Full pattern — user-customizable shortcut settings:
const [shortcuts, setShortcuts] = useState({ save: 'Mod+S', undo: 'Mod+Z' })
const [editingAction, setEditingAction] = useState(null)
const recorder = useHotkeyRecorder({
onRecord: (hotkey) => {
if (editingAction) {
setShortcuts(prev => ({ ...prev, [editingAction]: hotkey }))
setEditingAction(null)
}
},
onCancel: () => setEditingAction(null),
})
// Register actual hotkeys with current (possibly user-changed) bindings
useHotkey(shortcuts.save, () => save())
useHotkey(shortcuts.undo, () => undo())7. useHotkeySequenceRecorder — Record Multi-Step Sequences
Similar to useHotkeyRecorder but for recording key sequences (multiple steps).
8. useHotkeyRegistrations — Inspect All Active Shortcuts
Returns a live view of every registered hotkey and sequence. Useful for building shortcut palettes, help dialogs, or onboarding overlays:
import { useHotkeyRegistrations } from '@tanstack/react-hotkeys'
const { hotkeys } = useHotkeyRegistrations()
// hotkeys[0].options.meta?.name → 'Save'
// hotkeys[0].triggerCount → 3Key Options Explained
ignoreInputs — Smart Input Handling
Controls whether hotkeys fire when focus is inside <input>, <textarea>, or contenteditable elements.
Smart default behavior (when ignoreInputs is undefined):
Hotkeys with
Mod/Ctrlmodifiers (likeMod+S) do fire inside inputs — these are commands, not text entry.The
Escapekey does fire inside inputs.Single character keys (like
K,G) do not fire inside inputs — they'd conflict with typing.Button-type inputs allow hotkeys to fire.
You can override explicitly: ignoreInputs: true (never fire in inputs) or ignoreInputs: false (always fire).
requireReset — Prevent Key-Repeat Spam
When true, the hotkey fires once and won't fire again until all keys in the combination are released. Essential for toggle actions or commands that shouldn't repeat on hold.
target — Element Scoping
Scope a hotkey to a specific DOM element (only fires when that element or its children have focus):
const panelRef = useRef(null)
useHotkey('Escape', () => closePanel(), { target: panelRef })
return <div ref={panelRef} tabIndex={0}>...</div>enabled — Conditional Activation
Dynamically enable/disable hotkeys based on app state. Disabled hotkeys remain registered (visible in devtools) — only execution is suppressed:
useHotkey('Escape', () => closeModal(), { enabled: isModalOpen })conflictBehavior
What happens when two registrations use the same key combination. Options include 'warn' (default, logs a warning) and 'replace'.
Display & Formatting Utilities
formatForDisplay — Platform-Aware Rendering
Converts a hotkey string into a human-readable, platform-appropriate label:
import { formatForDisplay } from '@tanstack/react-hotkeys'
// On macOS:
formatForDisplay('Mod+S') // "⌘ S"
formatForDisplay('Mod+Shift+Z') // "⌘ ⇧ Z"
formatForDisplay('Control+Alt+D') // "⌃ ⌥ D"
// On Windows/Linux:
formatForDisplay('Mod+S') // "Ctrl+S"
formatForDisplay('Mod+Shift+Z') // "Ctrl+Shift+Z"Accepts an options object to override platform or switch between symbols and text labels.
Other Formatting Functions
formatHotkey— canonical string outputformatWithLabels— human-readable text labels (e.g. "Cmd" instead of ⌘)normalizeHotkey/normalizeHotkeyFromParsed/normalizeHotkeyFromEvent— produce canonical hotkey strings for storage and comparisonparseHotkey— parse a string into a structuredParsedHotkeyobject
Provider & Global Defaults
Wrap your app with HotkeysProvider to set defaults for all hooks:
import { HotkeysProvider } from '@tanstack/react-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkey: {
preventDefault: true,
ignoreInputs: false,
},
hotkeySequence: {
timeout: 1500,
},
hotkeyRecorder: {
onCancel: () => console.log('Recording cancelled'),
},
}}
>
<App />
</HotkeysProvider>Per-hook options always override provider defaults.
Devtools Integration
TanStack Hotkeys has dedicated devtools that let you inspect all registered hotkeys, see which keys are currently held, test shortcuts, and view trigger counts — all in real time:
import { TanStackDevtools } from '@tanstack/react-devtools'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
function App() {
return (
<div>
{/* Your app */}
<TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
</div>
)
}Framework Support
The core library (@tanstack/hotkeys) is framework-agnostic. Official adapters exist for:
Framework | Package |
|---|---|
React |
|
Preact |
|
Solid |
|
Svelte |
|
Angular |
|
Vue |
|
Lit |
|
Vanilla JS |
|
What's Achievable — Feature Checklist
Use this when evaluating ideas against the library's capabilities:
Feature Idea | Supported? | How |
|---|---|---|
Basic keyboard shortcuts (save, undo, etc.) | ✅ Yes |
|
Cross-platform modifier keys | ✅ Yes |
|
Shortcuts scoped to specific UI regions | ✅ Yes |
|
Conditional enable/disable | ✅ Yes |
|
Preventing conflicts with text input fields | ✅ Yes |
|
Single-fire (no key-repeat) | ✅ Yes |
|
Fire on key release instead of press | ✅ Yes |
|
Vim-style multi-step sequences (g → g) | ✅ Yes |
|
Detecting which keys are held down | ✅ Yes |
|
User-customizable shortcut recording | ✅ Yes |
|
User-customizable sequence recording | ✅ Yes |
|
Shortcut palette / help dialog data | ✅ Yes |
|
Platform-aware display labels (⌘ vs Ctrl) | ✅ Yes |
|
Global default configuration | ✅ Yes |
|
Conflict detection | ✅ Yes |
|
Devtools for debugging | ✅ Yes |
|
SSR compatibility | ✅ Yes | Built SSR-friendly by design |
Chords (two keys pressed simultaneously) | ✅ Yes |
|
Arbitrary key combos without modifiers | ✅ Yes | Single keys like |
Mouse-click shortcuts | ❌ No | Keyboard only |
Gamepad / controller input | ❌ No | Keyboard only |
Gesture recognition | ❌ No | Keyboard only |
Global OS-level hotkeys (outside browser) | ❌ No | Browser keyboard events only |
Patterns Worth Knowing
Command Palette Data Source
Combine meta on registrations with useHotkeyRegistrations to power a command palette:
useHotkey('Mod+S', () => save(), {
meta: { name: 'Save', description: 'Save the current document' },
})
// In your palette component:
const { hotkeys } = useHotkeyRegistrations()
// Each entry has .options.meta, .triggerCount, etc.Dynamic Shortcut Binding from User Preferences
const [userShortcuts, setUserShortcuts] = useState(loadFromStorage())
useHotkeys(
Object.entries(userShortcuts).map(([action, hotkey]) => ({
hotkey,
callback: () => executeAction(action),
options: {
meta: { name: action },
enabled: !isRecording,
},
}))
)Push-to-Talk Pattern
useHotkey('Space', () => startMic(), { eventType: 'keydown', requireReset: true })
useHotkey('Space', () => stopMic(), { eventType: 'keyup' })Modal Escape with Conditional Enable
useHotkey('Escape', () => closeModal(), { enabled: isModalOpen })Important Caveats
Alpha status — The library is under active development. APIs may change between releases. Pin your version in production.
preventDefaultistrueby default — This is intentional (most hotkeys override browser behavior like Ctrl+S). If you want the browser's default action to happen alongside your handler, explicitly setpreventDefault: false.stopPropagationistrueby default — Same reasoning. Override if you need the event to continue bubbling.Key detection uses
event.key— Withevent.codeas a fallback for letter and digit keys whenevent.keyproduces special characters (e.g. macOS Option+letter combos).Hotkeys are input-filtered by default — Single-key shortcuts won't fire inside text fields unless you opt out. Modifier combos and Escape do fire in inputs by default.
Quick Reference Links
Overview: https://tanstack.com/hotkeys/latest/docs/overview
React Quick Start: https://tanstack.com/hotkeys/latest/docs/framework/react/quick-start
Hotkeys Guide: https://tanstack.com/hotkeys/latest/docs/framework/react/guides/hotkeys
Sequences Guide: https://tanstack.com/hotkeys/latest/docs/framework/react/guides/sequences
Hotkey Recording Guide: https://tanstack.com/hotkeys/latest/docs/framework/react/guides/hotkey-recording
Sequence Recording Guide: https://tanstack.com/hotkeys/latest/docs/framework/react/guides/sequence-recording
Formatting & Display: https://tanstack.com/hotkeys/latest/docs/framework/react/guides/formatting-display
Devtools: https://tanstack.com/hotkeys/latest/docs/devtools
GitHub: https://github.com/TanStack/hotkeys