TanStack Hotkeys provides the useHotkeyRecorder hook for building keyboard shortcut customization UIs. This lets users record their own shortcuts by pressing the desired key combination, similar to how system preferences or IDE shortcut editors work.
import { useHotkeyRecorder, formatForDisplay } from '@tanstack/react-hotkeys'
function ShortcutRecorder() {
const { isRecording, recordedHotkey, startRecording, stopRecording, cancelRecording } =
useHotkeyRecorder({
onRecord: (hotkey) => {
console.log('Recorded:', hotkey) // e.g., "Mod+Shift+S"
},
})
return (
<div>
<button onClick={isRecording ? stopRecording : startRecording}>
{isRecording
? 'Press a key combination...'
: recordedHotkey
? formatForDisplay(recordedHotkey)
: 'Click to record'}
</button>
{isRecording && (
<button onClick={cancelRecording}>Cancel</button>
)}
</div>
)
}
The useHotkeyRecorder hook returns an object with:
| Property | Type | Description |
|---|---|---|
| isRecording | boolean | Whether the recorder is currently listening for key presses |
| recordedHotkey | Hotkey | null | The last recorded hotkey string, or null if nothing recorded |
| startRecording | () => void | Start listening for key presses |
| stopRecording | () => void | Stop listening and keep the recorded hotkey |
| cancelRecording | () => void | Stop listening and discard any recorded hotkey |
useHotkeyRecorder({
onRecord: (hotkey) => { /* called when a hotkey is recorded */ },
onCancel: () => { /* called when recording is cancelled */ },
onClear: () => { /* called when the recorded hotkey is cleared */ },
})
Called when the user presses a valid key combination (a modifier + a non-modifier key, or a single non-modifier key). Receives the recorded Hotkey string.
Called when recording is cancelled (either by pressing Escape or calling cancelRecording()).
Called when the recorded hotkey is cleared (by pressing Backspace or Delete during recording).
You can set default options for all useHotkeyRecorder calls by wrapping your component tree with HotkeysProvider. Per-hook options will override the provider defaults.
import { HotkeysProvider } from '@tanstack/react-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkeyRecorder: {
onCancel: () => console.log('Recording cancelled'),
},
}}
>
<App />
</HotkeysProvider>
The recorder has specific behavior for different keys:
| Key | Behavior |
|---|---|
| Modifier only (Shift, Ctrl, etc.) | Waits for a non-modifier key -- modifier-only presses don't complete a recording |
| Modifier + key (e.g., Ctrl+S) | Records the full combination |
| Single key (e.g., Escape, F1) | Records the single key |
| Escape | Cancels the recording |
| Backspace / Delete | Clears the currently recorded hotkey |
Recorded hotkeys automatically use the portable Mod format. If a user on macOS presses Command+S, the recorded hotkey will be Mod+S rather than Meta+S. This ensures shortcuts are portable across platforms.
Here's a more complete example of a shortcut customization panel:
import { useState } from 'react'
import {
useHotkey,
useHotkeyRecorder,
formatForDisplay,
} from '@tanstack/react-hotkeys'
import type { Hotkey } from '@tanstack/react-hotkeys'
function ShortcutSettings() {
const [shortcuts, setShortcuts] = useState<Record<string, Hotkey>>({
save: 'Mod+S',
undo: 'Mod+Z',
search: 'Mod+K',
})
const [editingAction, setEditingAction] = useState<string | null>(null)
const recorder = useHotkeyRecorder({
onRecord: (hotkey) => {
if (editingAction) {
setShortcuts((prev) => ({ ...prev, [editingAction]: hotkey }))
setEditingAction(null)
}
},
onCancel: () => setEditingAction(null),
})
// Register the actual hotkeys with their current bindings
useHotkey(shortcuts.save, () => save())
useHotkey(shortcuts.undo, () => undo())
useHotkey(shortcuts.search, () => openSearch())
return (
<div>
<h2>Keyboard Shortcuts</h2>
{Object.entries(shortcuts).map(([action, hotkey]) => (
<div key={action}>
<span>{action}</span>
<button
onClick={() => {
setEditingAction(action)
recorder.startRecording()
}}
>
{editingAction === action && recorder.isRecording
? 'Press keys...'
: formatForDisplay(hotkey)}
</button>
</div>
))}
</div>
)
}
The useHotkeyRecorder hook creates a HotkeyRecorder class instance and subscribes to its reactive state via @tanstack/react-store. The class manages its own keyboard event listeners and state, and the hook handles cleanup on unmount.