Function Signature
function useRecordHotkeys(
useKey?: boolean
): readonly [Set<string>, RecordingControls]
The useRecordHotkeys hook allows you to capture keyboard input from users, useful for building hotkey configuration interfaces or letting users define custom shortcuts.
Parameters
Whether to record the produced key (event.key) instead of the key code (event.code).When to use:
false (default): Records physical key positions (e.g., "KeyA", "ControlLeft")
true: Records the characters produced (e.g., "a", "Control")
Returns
The hook returns a tuple with two elements:
A Set containing all recorded keys since recording started or was last reset.Type:The Set automatically prevents duplicate keys from being recorded multiple times.
An object containing functions to control the recording process.Type:type RecordingControls = {
start: () => void
stop: () => void
resetKeys: () => void
isRecording: boolean
}
Recording Controls
Starts recording keyboard input.Behavior:
- Clears any previously recorded keys
- Stops any existing recording first
- Attaches a global keydown event listener to the document
- Sets
isRecording to true
- Prevents default behavior and stops propagation for all recorded keys
Stops recording keyboard input.Behavior:
- Removes the global keydown event listener
- Sets
isRecording to false
- Does not clear recorded keys (use
resetKeys for that)
Clears all recorded keys without stopping the recording.Useful when you want to let the user try again while keeping the recording active.
Boolean indicating whether recording is currently active.
Usage Examples
Basic Recording
import { useRecordHotkeys } from 'react-hotkeys-hook'
function HotkeyRecorder() {
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys()
return (
<div>
<button onClick={start} disabled={isRecording}>
Start Recording
</button>
<button onClick={stop} disabled={!isRecording}>
Stop Recording
</button>
<button onClick={resetKeys}>Clear Keys</button>
<div>
<strong>Status:</strong> {isRecording ? 'Recording...' : 'Stopped'}
</div>
<div>
<strong>Recorded Keys:</strong> {Array.from(keys).join(' + ')}
</div>
</div>
)
}
Hotkey Configuration UI
import { useState } from 'react'
import { useRecordHotkeys, useHotkeys } from 'react-hotkeys-hook'
function HotkeyConfigurator() {
const [savedHotkey, setSavedHotkey] = useState('')
const [keys, { start, stop, isRecording }] = useRecordHotkeys()
const handleSave = () => {
const hotkey = Array.from(keys).join('+')
setSavedHotkey(hotkey)
stop()
}
const handleCancel = () => {
stop()
}
// Use the configured hotkey
useHotkeys(savedHotkey || 'ctrl+k', () => {
console.log('Custom hotkey triggered!')
}, {
enabled: !!savedHotkey
})
return (
<div>
<h3>Configure Your Hotkey</h3>
{!isRecording ? (
<div>
<p>Current hotkey: <kbd>{savedHotkey || 'Not set'}</kbd></p>
<button onClick={start}>Record New Hotkey</button>
</div>
) : (
<div>
<p>Press your desired key combination...</p>
<p>
Recording: <kbd>{Array.from(keys).join(' + ') || 'Waiting...'}</kbd>
</p>
<button onClick={handleSave} disabled={keys.size === 0}>
Save
</button>
<button onClick={handleCancel}>Cancel</button>
</div>
)}
</div>
)
}
Using Character Keys Instead of Codes
import { useRecordHotkeys } from 'react-hotkeys-hook'
function CharacterRecorder() {
// Record actual characters instead of key codes
const [keys, { start, stop, isRecording }] = useRecordHotkeys(true)
return (
<div>
<button onClick={isRecording ? stop : start}>
{isRecording ? 'Stop' : 'Start'} Recording
</button>
<div>
{/* With useKey=true, shows actual characters like 'A', 'Shift' */}
{/* With useKey=false, shows codes like 'KeyA', 'ShiftLeft' */}
Recorded: {Array.from(keys).join(' + ')}
</div>
</div>
)
}
import { useState } from 'react'
import { useRecordHotkeys } from 'react-hotkeys-hook'
type HotkeyConfig = {
combination: string
description: string
}
function HotkeyManager() {
const [hotkeys, setHotkeys] = useState<HotkeyConfig[]>([])
const [description, setDescription] = useState('')
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys()
const handleAdd = () => {
if (keys.size === 0) return
const combination = Array.from(keys).join('+')
setHotkeys([...hotkeys, { combination, description }])
// Reset for next entry
resetKeys()
setDescription('')
stop()
}
const handleDelete = (index: number) => {
setHotkeys(hotkeys.filter((_, i) => i !== index))
}
return (
<div>
<h2>Hotkey Manager</h2>
<div>
<h3>Add New Hotkey</h3>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isRecording}
/>
{!isRecording ? (
<button onClick={start}>Record Hotkey</button>
) : (
<div>
<p>Press keys: <kbd>{Array.from(keys).join(' + ')}</kbd></p>
<button onClick={handleAdd} disabled={keys.size === 0}>
Add Hotkey
</button>
<button onClick={() => { resetKeys(); stop(); }}>
Cancel
</button>
</div>
)}
</div>
<div>
<h3>Configured Hotkeys</h3>
<table>
<thead>
<tr>
<th>Combination</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{hotkeys.map((hotkey, index) => (
<tr key={index}>
<td><kbd>{hotkey.combination}</kbd></td>
<td>{hotkey.description}</td>
<td>
<button onClick={() => handleDelete(index)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
Auto-Stop After Timeout
import { useEffect } from 'react'
import { useRecordHotkeys } from 'react-hotkeys-hook'
function TimedRecorder() {
const [keys, { start, stop, isRecording }] = useRecordHotkeys()
// Auto-stop recording after 5 seconds
useEffect(() => {
if (!isRecording) return
const timeout = setTimeout(() => {
stop()
alert('Recording stopped automatically')
}, 5000)
return () => clearTimeout(timeout)
}, [isRecording, stop])
return (
<div>
<button onClick={start} disabled={isRecording}>
Start Recording (5s timeout)
</button>
{isRecording && (
<p>Recording... (auto-stops in 5 seconds)</p>
)}
<p>Keys: {Array.from(keys).join(' + ')}</p>
</div>
)
}
import { useRecordHotkeys } from 'react-hotkeys-hook'
function HotkeyConverter() {
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys()
// Convert recorded keys to useHotkeys format
const getHotkeyString = () => {
const keyArray = Array.from(keys)
// Sort modifiers first
const modifiers = keyArray.filter(k =>
['Control', 'Alt', 'Shift', 'Meta'].includes(k)
).map(k => k.toLowerCase())
const regularKeys = keyArray.filter(k =>
!['Control', 'Alt', 'Shift', 'Meta'].includes(k)
).map(k => k.toLowerCase())
return [...modifiers, ...regularKeys].join('+')
}
return (
<div>
<button onClick={isRecording ? stop : start}>
{isRecording ? 'Stop' : 'Start'}
</button>
<button onClick={resetKeys}>Reset</button>
<div>
<strong>Raw keys:</strong> {Array.from(keys).join(', ')}
</div>
<div>
<strong>Hotkey format:</strong> {getHotkeyString()}
</div>
<div>
<strong>Usage:</strong>
<pre>
{`useHotkeys('${getHotkeyString()}', callback)`}
</pre>
</div>
</div>
)
}
Important Notes
When recording is active, all keyboard events are prevented from their default behavior and stopped from propagating. This means users won’t be able to type in inputs or trigger other shortcuts while recording.
The keys Set prevents duplicate entries automatically. If a user presses the same key multiple times while recording, it will only appear once in the Set.
Recording always happens at the document level, not scoped to specific elements. This ensures all keyboard input is captured regardless of focus.
Synthetic keyboard events (like those from Chrome autofill with event.code === undefined) are automatically ignored and won’t be recorded.
See Also