tanstack-hotkeys
Type-safe keyboard shortcuts for the web. TanStack Hotkeys provides template-string bindings with full TypeScript autocomplete, a cross-platform `Mod` key that maps to Command on macOS and Control on
TanStack Hotkeys
Type-safe keyboard shortcuts for the web. TanStack Hotkeys provides template-string bindings with full TypeScript autocomplete, a cross-platform Mod key that maps to Command on macOS and Control on Windows/Linux, a singleton Hotkey Manager, multi-key sequences (Vim-style), hotkey recording for settings UIs, key state tracking, and platform-aware display formatting — all SSR-safe.
Note: TanStack Hotkeys is currently in pre-alpha (v0.1.0). The API may change.
Quick References
| File | Purpose |
|---|---|
packages/hotkeys/src/index.ts | Core package entry point (all framework-agnostic exports) |
packages/react-hotkeys/src/index.ts | React adapter entry point (re-exports core + React hooks) |
packages/hotkeys/src/hotkey.ts | All TypeScript types (Hotkey, ParsedHotkey, RawHotkey, etc.) |
docs/framework/react/quick-start.md | React quick start guide |
docs/framework/react/guides/hotkeys.md | In-depth useHotkey guide with all options |
Packages
| Package | npm name | Description |
|---|---|---|
packages/hotkeys | @tanstack/hotkeys | Core framework-agnostic library (HotkeyManager, parsing, matching, formatting, sequences, recorder, key state tracker) |
packages/react-hotkeys | @tanstack/react-hotkeys | React adapter (hooks: useHotkey, useHotkeySequence, useHotkeyRecorder, useHeldKeys, useKeyHold, useHeldKeyCodes) |
packages/hotkeys-devtools | @tanstack/hotkeys-devtools | Framework-agnostic devtools panel |
packages/react-hotkeys-devtools | @tanstack/react-hotkeys-devtools | React devtools plugin for TanStack DevTools |
Each framework adapter package re-exports everything from @tanstack/hotkeys, so you only need to install the adapter.
When to Use
- You need type-safe keyboard shortcuts with full TypeScript autocomplete for key combinations
- You want cross-platform shortcuts (
Mod+S→ Command+S on Mac, Ctrl+S on Windows/Linux) without writing platform detection code - You need a centralized hotkey manager with conflict detection, scoped targets, and automatic input-element filtering
- You're building settings UIs that let users record/customize their own keyboard shortcuts
- You need Vim-style multi-key sequences (e.g.,
g g,d i w) - You want to display hotkeys in a platform-aware format (⌘⇧S on Mac, Ctrl+Shift+S on Windows)
- You need real-time key state tracking for power-user UIs (e.g., showing held modifier indicators)
Installation
# React (includes core)
npm install @tanstack/react-hotkeys
# Core only (vanilla JS, no framework)
npm install @tanstack/hotkeys
# Devtools (optional, dev dependency)
npm install -D @tanstack/react-hotkeys-devtools
Best Practices
- Use
Modinstead ofControlorMetafor cross-platform shortcuts.Mod+Sautomatically resolves to Command+S on macOS and Ctrl+S on Windows/Linux. - Don't wrap callbacks in
useCallback— theuseHotkeyhook syncs callbacks on every render to prevent stale closures. Your callback always has access to the latest React state. - Use
enabledoption for conditional hotkeys instead of conditionally calling the hook. This keeps hook call order stable (Rules of Hooks). - Set
tabIndexon elements used astargetrefs so they can receive keyboard events. - Avoid
Shift+punctuationcombinations (e.g.,Shift+,) as they produce different characters on different keyboard layouts. The type system prevents this. - Use
conflictBehavior: 'replace'when you intentionally want to override an existing hotkey from another component, or'allow'to suppress warnings when multiple handlers are expected. - Use
formatForDisplayfor UI labels rather than hardcoding key names — it renders platform-native symbols (⌘⇧S on Mac vs Ctrl+Shift+S on Windows).
Common Patterns
Basic hotkey registration (React):
import { useHotkey } from '@tanstack/react-hotkeys'
function App() {
useHotkey('Mod+S', (event, { hotkey, parsedHotkey }) => {
saveDocument()
})
useHotkey('Escape', () => {
closeDialog()
})
return <div>Press Cmd+S (Mac) or Ctrl+S (Windows) to save</div>
}
Using RawHotkey object instead of string:
useHotkey({ key: 'S', mod: true }, () => save())
useHotkey({ key: 'S', mod: true, shift: true }, () => saveAs())
useHotkey({ key: 'Escape' }, () => closeModal())
Conditional enable/disable:
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
useHotkey('Escape', () => onClose(), { enabled: isOpen })
if (!isOpen) return null
return <div className="modal">Press Escape to close</div>
}
Scoped to a specific element:
import { useRef } from 'react'
import { useHotkey } from '@tanstack/react-hotkeys'
function Panel() {
const panelRef = useRef<HTMLDivElement>(null)
useHotkey('Escape', () => closePanel(), { target: panelRef })
return (
<div ref={panelRef} tabIndex={0}>
<p>Press Escape while focused here</p>
</div>
)
}
Vim-style multi-key sequences:
import { useHotkeySequence } from '@tanstack/react-hotkeys'
function VimEditor() {
useHotkeySequence(['G', 'G'], () => scrollToTop())
useHotkeySequence(['D', 'D'], () => deleteLine())
useHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord(), { timeout: 500 })
return <div>Editor with Vim shortcuts</div>
}
Recording hotkeys for settings UIs:
import { useState } from 'react'
import { useHotkeyRecorder } from '@tanstack/react-hotkeys'
import type { Hotkey } from '@tanstack/react-hotkeys'
function ShortcutSettings() {
const [shortcut, setShortcut] = useState<Hotkey>('Mod+S')
const recorder = useHotkeyRecorder({
onRecord: (hotkey) => setShortcut(hotkey),
onCancel: () => console.log('Cancelled'),
onClear: () => console.log('Cleared'),
})
return (
<div>
<span>Current: {shortcut}</span>
<button onClick={recorder.startRecording}>
{recorder.isRecording ? 'Press keys...' : 'Edit Shortcut'}
</button>
</div>
)
}
Tracking held keys:
import { useKeyHold, useHeldKeys } from '@tanstack/react-hotkeys'
function StatusBar() {
const isShiftHeld = useKeyHold('Shift')
const heldKeys = useHeldKeys()
return (
<div>
{isShiftHeld && <span>Shift mode active</span>}
{heldKeys.length > 0 && <span>Keys: {heldKeys.join('+')}</span>}
</div>
)
}
Platform-aware display formatting:
import { useHotkey, formatForDisplay } from '@tanstack/react-hotkeys'
function SaveButton() {
useHotkey('Mod+S', () => save())
return (
<button>
Save <kbd>{formatForDisplay('Mod+S')}</kbd>
{/* Mac: "⌘S" | Windows: "Ctrl+S" */}
</button>
)
}
Global default options with HotkeysProvider:
import { HotkeysProvider } from '@tanstack/react-hotkeys'
function Root() {
return (
<HotkeysProvider
defaultOptions={{
hotkey: { preventDefault: true },
hotkeySequence: { timeout: 1500 },
}}
>
<App />
</HotkeysProvider>
)
}
Vanilla JS (without React):
import { getHotkeyManager, formatForDisplay } from '@tanstack/hotkeys'
const manager = getHotkeyManager()
const handle = manager.register('Mod+S', (event, context) => {
console.log('Save triggered!')
})
// Update callback without re-registering
handle.callback = newCallback
// Update options
handle.setOptions({ enabled: false })
// Check state
console.log(handle.isActive) // true
console.log(manager.isRegistered('Mod+S')) // true
// Unregister
handle.unregister()
Devtools setup:
import { TanStackDevtools } from '@tanstack/react-devtools'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
function App() {
return (
<>
{/* Your app */}
<TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
</>
)
}
API Quick Reference
Core (@tanstack/hotkeys)
| Export | Type | Description |
|---|---|---|
HotkeyManager | Class | Singleton manager for hotkey registrations with per-target listeners |
getHotkeyManager() | Function | Gets the singleton HotkeyManager instance |
KeyStateTracker | Class | Singleton tracker for currently held keyboard keys |
getKeyStateTracker() | Function | Gets the singleton KeyStateTracker instance |
HotkeyRecorder | Class | Framework-agnostic class for recording keyboard shortcuts |
SequenceManager | Class | Singleton manager for Vim-style multi-key sequences |
getSequenceManager() | Function | Gets the singleton SequenceManager instance |
parseHotkey(hotkey, platform?) | Function | Parses a hotkey string into a ParsedHotkey object |
rawHotkeyToParsedHotkey(raw, platform?) | Function | Converts a RawHotkey object to ParsedHotkey |
normalizeHotkey(hotkey, platform?) | Function | Normalizes a hotkey string to canonical form |
parseKeyboardEvent(event) | Function | Parses a KeyboardEvent into a ParsedHotkey |
keyboardEventToHotkey(event) | Function | Converts a KeyboardEvent to a hotkey string |
isModifier(key) | Function | Checks if a string is a modifier key name |
isModifierKey(event) | Function | Checks if a KeyboardEvent is a modifier-only key press |
hasNonModifierKey(hotkey, platform?) | Function | Checks if a hotkey contains at least one non-modifier key |
convertToModFormat(hotkey, platform?) | Function | Converts platform-specific modifiers to portable Mod format |
matchesKeyboardEvent(event, hotkey, platform?) | Function | Checks if a KeyboardEvent matches a hotkey |
createHotkeyHandler(hotkey, callback, options?) | Function | Creates a keyboard event handler for a single hotkey |
createMultiHotkeyHandler(handlers, options?) | Function | Creates a handler that matches multiple hotkeys |
createSequenceMatcher(sequence, options?) | Function | Creates a simple sequence matcher for one-off use |
formatHotkey(parsed) | Function | Converts a ParsedHotkey to a canonical hotkey string |
formatForDisplay(hotkey, options?) | Function | Formats a hotkey for platform-aware UI display (⌘S on Mac, Ctrl+S on Windows) |
formatWithLabels(hotkey, platform?) | Function | Formats with human-readable labels (Cmd+S on Mac, Ctrl+S on Windows) |
formatKeyForDebuggingDisplay(key, options?) | Function | Formats a single key for devtools/debugging display |
validateHotkey(hotkey) | Function | Validates a hotkey string, returns ValidationResult |
assertValidHotkey(hotkey) | Function | Validates a hotkey and throws on error |
checkHotkey(hotkey) | Function | Validates and logs warnings, returns boolean |
detectPlatform() | Function | Detects current platform: 'mac', 'windows', or 'linux' |
resolveModifier(modifier, platform?) | Function | Resolves 'Mod' to 'Meta' or 'Control' based on platform |
normalizeKeyName(key) | Function | Normalizes key aliases to canonical form (e.g., 'Esc' → 'Escape') |
React (@tanstack/react-hotkeys)
| Export | Type | Description |
|---|---|---|
useHotkey(hotkey, callback, options?) | Hook | Registers a keyboard shortcut with automatic lifecycle management |
useHotkeySequence(sequence, callback, options?) | Hook | Registers a multi-key Vim-style sequence |
useHotkeyRecorder(options) | Hook | Returns recording state and controls for capturing keyboard shortcuts |
useHeldKeys() | Hook | Returns array of currently held key names (reactive) |
useHeldKeyCodes() | Hook | Returns map of held key names to event.code values |
useKeyHold(key) | Hook | Returns boolean for whether a specific key is held |
HotkeysProvider | Component | Context provider for setting global default options |
useHotkeysContext() | Hook | Access the HotkeysProvider context |
Devtools (@tanstack/react-hotkeys-devtools)
| Export | Type | Description |
|---|---|---|
hotkeysDevtoolsPlugin() | Function | Creates a plugin for @tanstack/react-devtools TanStackDevtools component |
HotkeysDevtoolsPanel | Component | Standalone devtools panel (auto-noop in production) |
Key Types
| Type | Description |
|---|---|
Hotkey | Type-safe union of all valid hotkey strings with autocomplete (e.g., 'Mod+S', 'Control+Shift+A', 'Escape') |
RawHotkey | Object form for programmatic hotkey registration: { key, mod?, ctrl?, shift?, alt?, meta? } |
RegisterableHotkey | Hotkey | RawHotkey — accepted by useHotkey and HotkeyManager.register() |
ParsedHotkey | Parsed representation: { key, ctrl, shift, alt, meta, modifiers } |
HotkeyCallback | (event: KeyboardEvent, context: HotkeyCallbackContext) => void |
HotkeyCallbackContext | { hotkey: Hotkey, parsedHotkey: ParsedHotkey } |
HotkeyOptions | Options for HotkeyManager.register(): enabled, preventDefault, stopPropagation, eventType, requireReset, ignoreInputs, conflictBehavior, target, platform |
UseHotkeyOptions | React version of HotkeyOptions — target also accepts React.RefObject |
HotkeyRegistrationHandle | Handle returned by HotkeyManager.register() with unregister(), setOptions(), callback setter, isActive |
HotkeySequence | Array<Hotkey> — sequence of hotkeys for Vim-style shortcuts |
SequenceOptions | Extends HotkeyOptions with timeout (ms between keys, default 1000) |
ConflictBehavior | 'warn' | 'error' | 'replace' | 'allow' |
ValidationResult | { valid: boolean, warnings: string[], errors: string[] } |
HeldKey | CanonicalModifier | Key — keys trackable as "held" |
FormatDisplayOptions | { platform?: 'mac' | 'windows' | 'linux' } |
Default Option Values
| Option | Default | Notes |
|---|---|---|
enabled | true | |
preventDefault | true | Prevents browser defaults like Ctrl+S save dialog |
stopPropagation | true | |
eventType | 'keydown' | Can be 'keyup' |
requireReset | false | When true, fires only once per press until key release |
ignoreInputs | Smart default | false for Ctrl/Meta shortcuts and Escape; true for single keys and Shift/Alt combos |
conflictBehavior | 'warn' | |
target | document | |
platform | Auto-detected | 'mac', 'windows', or 'linux' |
Sequence timeout | 1000 (ms) | Time between keys in a sequence |