Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Hotkeys API Reference
Hotkey Sequence API Reference
Key hold & held keys API Reference
Hotkey Recorder API Reference
Hotkey Sequence Recorder API Reference
Normalization & format API Reference
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>
  )
}
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 */ },
})
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>
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

ignoreInputs

The HotkeyRecorderOptions supports an ignoreInputs option (defaults to true). When true, the recorder will not intercept normal typing in text inputs, textareas, selects, or contentEditable elements -- keystrokes pass through to the input as usual. Pressing Escape still cancels recording even when focused on an input. Set ignoreInputs: false if you want the recorder to capture keys from within input elements.

tsx
createHotkeyRecorder({
  ignoreInputs: false, // record even from inside inputs
  onRecord: (hotkey) => console.log(hotkey),
})
createHotkeyRecorder({
  ignoreInputs: false, // record even from inside inputs
  onRecord: (hotkey) => console.log(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>
  )
}
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.