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
API Reference
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

Sequence Recording Guide

TanStack Hotkeys provides the HotkeySequenceRecorderController for building UIs where users record multi-chord sequences (Vim-style shortcuts). Each step is captured like a single hotkey chord; users finish with Enter by default, or you can use manual commit and optional idle timeout.

Basic Usage

ts
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { HotkeySequenceRecorderController, formatForDisplay } from '@tanstack/lit-hotkeys'
import type { HotkeySequence } from '@tanstack/lit-hotkeys'

@customElement('sequence-recorder')
class SequenceRecorder extends LitElement {
  private recorder = new HotkeySequenceRecorderController(this, {
    onRecord: (sequence: HotkeySequence) => {
      console.log('Recorded:', sequence) // e.g., ["G", "G"]
    },
  })

  render() {
    const { isRecording, steps, recordedSequence } = this.recorder
    return html`
      <div>
        <button
          @click=${() =>
            isRecording
              ? this.recorder.cancelRecording()
              : this.recorder.startRecording()}
        >
          ${isRecording
            ? 'Press chords, then Enter…'
            : recordedSequence
              ? recordedSequence.map((h) => formatForDisplay(h)).join(' ')
              : 'Click to record'}
        </button>
        ${isRecording
          ? html`<button @click=${() => this.recorder.cancelRecording()}>
              Cancel
            </button>`
          : nothing}
      </div>
    `
  }
}

Controller API

HotkeySequenceRecorderController exposes the following reactive getters and methods:

MemberTypeDescription
isRecordingbooleanWhether the recorder is currently listening
stepsHotkeySequenceChords captured in the current session
recordedSequenceHotkeySequence | nullLast committed sequence
startRecording()() => voidStart a new recording session
stopRecording()() => voidStop without calling onRecord
cancelRecording()() => voidStop and call onCancel
commitRecording()() => voidCommit current steps (no-op if empty)
setOptions(opts)(Partial<HotkeySequenceRecorderOptions>) => voidUpdate options at runtime

The controller registers itself with the host in its constructor, subscribes to the underlying HotkeySequenceRecorder store on hostConnected, and cleans up on hostDisconnected.

Options

Pass options as the second argument to the constructor:

ts
new HotkeySequenceRecorderController(this, {
  onRecord: (sequence) => { /* called when a sequence is committed */ },
  onCancel: () => { /* called when recording is cancelled */ },
  onClear: () => { /* called when cleared via Backspace with no steps */ },
  commitKeys: 'enter', // or 'none'
  idleTimeoutMs: 2000,
})

onRecord

Called when a sequence is committed (including [] when cleared via Backspace with no steps). Receives the recorded HotkeySequence array.

onCancel

Called when recording is cancelled (either by pressing Escape or calling cancelRecording()).

onClear

Called when the sequence is cleared (by pressing Backspace or Delete during recording when no steps remain).

commitKeys

Controls how the user finishes recording from the keyboard:

  • 'enter' (default) — plain Enter (no modifiers) commits when at least one step exists.
  • 'none' — only commitRecording() or idleTimeoutMs finishes recording; plain Enter can be recorded as a chord.

commitOnEnter

When commitKeys is 'enter', set to false to treat Enter as a normal chord. Use commitRecording() or idle timeout to finish instead.

idleTimeoutMs

Milliseconds of inactivity after the last completed chord to auto-commit. The timer does not run while waiting for the first chord.

ignoreInputs

The HotkeySequenceRecorderOptions 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.

ts
new HotkeySequenceRecorderController(this, {
  ignoreInputs: false, // record even from inside inputs
  onRecord: (sequence) => console.log(sequence),
})

Recording Behavior

InputBehavior
Valid chordAppended to steps; listener stays active
Enter (no modifiers), commitKeys: 'enter', steps.length >= 1Commits and calls onRecord
EscapeCancels; calls onCancel
Backspace / Delete (no modifiers)Removes last step, or if empty runs onClear + onRecord([]) and stops

Recorded chords use portable Mod format, same as HotkeyRecorderController.

Under the Hood

The HotkeySequenceRecorderController creates a HotkeySequenceRecorder class instance and subscribes to its reactive state via @tanstack/store. The class manages its own keyboard event listeners and state, and the controller handles cleanup on disconnect.