Skip to main content

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

useKey
boolean
default:"false"
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:
keys
Set<string>
A Set containing all recorded keys since recording started or was last reset.Type:
Set<string>
The Set automatically prevents duplicate keys from being recorded multiple times.
controls
RecordingControls
An object containing functions to control the recording process.Type:
type RecordingControls = {
  start: () => void
  stop: () => void
  resetKeys: () => void
  isRecording: boolean
}

Recording Controls

start
() => void
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
stop
() => void
Stops recording keyboard input.Behavior:
  • Removes the global keydown event listener
  • Sets isRecording to false
  • Does not clear recorded keys (use resetKeys for that)
resetKeys
() => void
Clears all recorded keys without stopping the recording.Useful when you want to let the user try again while keeping the recording active.
isRecording
boolean
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>
  )
}

Advanced Configuration Form

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>
  )
}

Converting to useHotkeys Format

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

Build docs developers (and LLMs) love