Guides

Hotkey Recording Guide

TanStack Hotkeys provides the createHotkeyRecorder primitive 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.

Basic Usage

tsx
import { createHotkeyRecorder, formatForDisplay } from '@tanstack/solid-hotkeys'

function ShortcutRecorder() {
  const recorder = createHotkeyRecorder({
    onRecord: (hotkey) => {
      console.log('Recorded:', hotkey) // e.g., "Mod+Shift+S"
    },
  })

  return (
    <div>
      <button onClick={() => recorder.isRecording() ? recorder.stopRecording() : recorder.startRecording()}>
        {recorder.isRecording()
          ? 'Press a key combination...'
          : recorder.recordedHotkey()
            ? formatForDisplay(recorder.recordedHotkey()!)
            : 'Click to record'}
      </button>
      <Show when={recorder.isRecording()}>
        <button onClick={recorder.cancelRecording}>Cancel</button>
      </Show>
    </div>
  )
}
Note

In Solid, isRecording and recordedHotkey are accessors (signal getters). You must call them with () to read the value: recorder.isRecording(), recorder.recordedHotkey().

Return Value

The createHotkeyRecorder primitive returns an object with:

PropertyTypeDescription
isRecording() => booleanAccessor returning whether the recorder is currently listening
recordedHotkey() => Hotkey | nullAccessor returning the last recorded hotkey, or null
startRecording() => voidStart listening for key presses
stopRecording() => voidStop listening and keep the recorded hotkey
cancelRecording() => voidStop listening and discard any recorded hotkey

Options

tsx
createHotkeyRecorder({
  onRecord: (hotkey) => { /* called when a hotkey is recorded */ },
  onCancel: () => { /* called when recording is cancelled */ },
  onClear: () => { /* called when the recorded hotkey is cleared */ },
})

Options can also be passed as an accessor function for reactive configuration.

onRecord

Called when the user presses a valid key combination. Receives the recorded Hotkey string.

onCancel

Called when recording is cancelled (Escape or cancelRecording()).

onClear

Called when the recorded hotkey is cleared (Backspace or Delete during recording).

Global Default Options via Provider

tsx
import { HotkeysProvider } from '@tanstack/solid-hotkeys'

<HotkeysProvider
  defaultOptions={{
    hotkeyRecorder: {
      onCancel: () => console.log('Recording cancelled'),
    },
  }}
>
  <App />
</HotkeysProvider>

Recording Behavior

KeyBehavior
Modifier onlyWaits for a non-modifier key
Modifier + keyRecords the full combination
Single keyRecords the single key
EscapeCancels the recording
Backspace / DeleteClears the currently recorded hotkey

Mod Auto-Conversion

Recorded hotkeys automatically use the portable Mod format (Command on Mac, Control elsewhere).

Building a Shortcut Settings UI

tsx
import { createSignal } from 'solid-js'
import {
  createHotkey,
  createHotkeyRecorder,
  formatForDisplay,
} from '@tanstack/solid-hotkeys'
import type { Hotkey } from '@tanstack/solid-hotkeys'

function ShortcutSettings() {
  const [shortcuts, setShortcuts] = createSignal<Record<string, Hotkey>>({
    save: 'Mod+S',
    undo: 'Mod+Z',
    search: 'Mod+K',
  })

  const [editingAction, setEditingAction] = createSignal<string | null>(null)

  const recorder = createHotkeyRecorder({
    onRecord: (hotkey) => {
      const action = editingAction()
      if (action) {
        setShortcuts((prev) => ({ ...prev, [action]: hotkey }))
        setEditingAction(null)
      }
    },
    onCancel: () => setEditingAction(null),
  })

  // Register the actual hotkeys with their current bindings
  createHotkey(() => shortcuts().save, () => save())
  createHotkey(() => shortcuts().undo, () => undo())
  createHotkey(() => shortcuts().search, () => openSearch())

  return (
    <div>
      <h2>Keyboard Shortcuts</h2>
      <For each={Object.entries(shortcuts())}>
        {([action, hotkey]) => (
          <div>
            <span>{action}</span>
            <button
              onClick={() => {
                setEditingAction(action)
                recorder.startRecording()
              }}
            >
              {editingAction() === action && recorder.isRecording()
                ? 'Press keys...'
                : formatForDisplay(hotkey)}
            </button>
          </div>
        )}
      </For>
    </div>
  )
}

Under the Hood

The createHotkeyRecorder primitive creates a HotkeyRecorder class instance and subscribes to its reactive state via @tanstack/solid-store. The class manages its own keyboard event listeners and state, and the primitive handles cleanup when the component is disposed.