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
App Hotkeys API Reference
Key hold & held keys API Reference
Hotkey Recorder API Reference
Normalization & format API Reference

Svelte Example: Create Hotkey Recorder Sequences

<script lang="ts">
  import {
    createHotkeyRecorder,
    createHotkeys,
    formatForDisplay,
    getHotkeyRegistrations,
  } from '@tanstack/svelte-hotkeys'
  import type { Hotkey } from '@tanstack/svelte-hotkeys'
  import ShortcutListItem from './ShortcutListItem.svelte'

  interface Shortcut {
    id: string
    name: string
    description: string
    /** A chord or a sequence; null when unassigned */
    hotkey: Hotkey | null
  }

  let nextId = 0
  function createId(): string {
    return `shortcut_${++nextId}`
  }

  let shortcuts = $state<Array<Shortcut>>([
    {
      id: createId(),
      name: 'Save',
      description: 'Save the current document',
      hotkey: 'Mod+S',
    },
    {
      id: createId(),
      name: 'Open (gg)',
      description: 'Open the file browser',
      hotkey: ['G', 'G'],
    },
    {
      id: createId(),
      name: 'New (dd)',
      description: 'Create a new document',
      hotkey: ['D', 'D'],
    },
    {
      id: createId(),
      name: 'Close',
      description: 'Close the current tab',
      hotkey: 'Mod+Shift+K',
    },
    {
      id: createId(),
      name: 'Undo (yy)',
      description: 'Undo the last action',
      hotkey: ['Y', 'Y'],
    },
    {
      id: createId(),
      name: 'Redo',
      description: 'Redo the last undone action',
      hotkey: 'Mod+Shift+G',
    },
  ])

  let editingId = $state<string | null>(null)
  let draftName = $state('')
  let draftDescription = $state('')

  const recorder = createHotkeyRecorder({
    // Record up to four chords; Enter or the fourth chord commits.
    maxSteps: 4,
    onRecord: (hotkey: Hotkey) => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                hotkey,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
    onCancel: () => {
      if (editingId) {
        const shortcut = shortcuts.find((s) => s.id === editingId)
        if (shortcut && shortcut.hotkey === null) {
          shortcuts = shortcuts.filter((s) => s.id !== editingId)
        }
      }
      editingId = null
    },
    onClear: () => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                hotkey: null,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
  })

  let isRecording = $derived(recorder.isRecording)

  // Register every assigned shortcut, chords and sequences alike, with meta
  createHotkeys(() =>
    shortcuts.flatMap((s) =>
      s.hotkey === null
        ? []
        : [
            {
              hotkey: s.hotkey,
              callback: () => {
                console.log(`${s.name} triggered:`, s.hotkey)
              },
              options: {
                meta: { name: s.name, description: s.description },
              },
            },
          ],
    ),
  )

  function handleEdit(id: string) {
    const shortcut = shortcuts.find((s) => s.id === id)
    if (!shortcut) return
    editingId = id
    draftName = shortcut.name
    draftDescription = shortcut.description
    recorder.startRecording()
  }

  function handleSaveEditing() {
    if (editingId) {
      shortcuts = shortcuts.map((s) =>
        s.id === editingId
          ? { ...s, name: draftName, description: draftDescription }
          : s,
      )
      recorder.stopRecording()
      editingId = null
    }
  }

  function handleCancel() {
    recorder.cancelRecording()
  }

  function handleDelete(id: string) {
    shortcuts = shortcuts.filter((s) => s.id !== id)
  }

  function handleCreateNew() {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      hotkey: null,
    }
    shortcuts = [...shortcuts, newShortcut]
    editingId = newShortcut.id
    draftName = ''
    draftDescription = ''
    recorder.startRecording()
  }

  // Registrations viewer
  const registrations = getHotkeyRegistrations()
</script>

<div class="app">
  <header>
    <h1>Recording Sequences</h1>
    <p>
      <code>createHotkeyRecorder</code> with <code>maxSteps</code> records up to that
      many chords. Click Edit, press each chord in order, then press Enter (or reach
      four chords) to save. One chord saves as a regular hotkey. Escape cancels; Backspace
      removes the last chord or clears when empty.
    </p>
  </header>

  <main>
    <section class="demo-section">
      <h2>Shortcuts</h2>
      <div class="shortcuts-list">
        {#each shortcuts as shortcut (shortcut.id)}
          <ShortcutListItem
            {shortcut}
            isEditing={editingId === shortcut.id}
            draftName={editingId === shortcut.id ? draftName : shortcut.name}
            draftDescription={editingId === shortcut.id
              ? draftDescription
              : shortcut.description}
            onDraftNameChange={(v) => (draftName = v)}
            onDraftDescriptionChange={(v) => (draftDescription = v)}
            liveSteps={recorder.steps}
            onEdit={() => handleEdit(shortcut.id)}
            onSave={handleSaveEditing}
            onCancel={handleCancel}
            onDelete={() => handleDelete(shortcut.id)}
          />
        {/each}
      </div>
      <button
        type="button"
        class="create-button"
        onclick={handleCreateNew}
        disabled={isRecording}
      >
        + Create New Shortcut
      </button>
    </section>

    {#if recorder.isRecording}
      <div class="info-box recording-notice">
        <strong>Recording...</strong> Press up to four chords, then Enter to
        finish. Escape cancels. Backspace removes the last chord or clears.
        {#if recorder.steps.length > 0}
          <div>
            Steps:
            <kbd>{formatForDisplay(recorder.steps)}</kbd>
          </div>
        {/if}
      </div>
    {/if}

    <!-- Registrations Viewer -->
    <section class="demo-section">
      <h2>Live Registrations</h2>
      <p>
        This table is powered by <code>getHotkeyRegistrations()</code> — trigger counts,
        names, and descriptions update in real-time as you use your shortcuts.
      </p>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Hotkey</th>
            <th>Name</th>
            <th>Description</th>
            <th>Progress</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          {#each registrations.hotkeys as reg (reg.id)}
            <tr>
              <td>
                <kbd>{formatForDisplay(reg.hotkey)}</kbd>
              </td>
              <td>{reg.options.meta?.name ?? '—'}</td>
              <td class="description-cell">
                {reg.options.meta?.description ?? '—'}
              </td>
              <td>
                {reg.kind === 'sequence'
                  ? `${reg.matchedStepCount}/${reg.hotkey.length}`
                  : '—'}
              </td>
              <td>
                <span
                  class={reg.options.enabled !== false
                    ? 'status-on'
                    : 'status-off'}
                >
                  {reg.options.enabled !== false ? 'yes' : 'no'}
                </span>
              </td>
              <td class="trigger-count">{reg.triggerCount}</td>
            </tr>
          {/each}
          {#if registrations.hotkeys.length === 0}
            <tr>
              <td colspan="6" class="hint">No hotkeys registered</td>
            </tr>
          {/if}
        </tbody>
      </table>
    </section>

    <section class="demo-section">
      <h2>Usage</h2>
      <pre class="code-block">{`import {
  createHotkeys,
  createHotkeyRecorder,
  getHotkeyRegistrations,
} from '@tanstack/svelte-hotkeys'

// Record a chord or a sequence of up to four chords
const recorder = createHotkeyRecorder({
  maxSteps: 4,
  onRecord: (hotkey) => save(hotkey), // 'Mod+S' or ['G', 'G']
})

// Register chords and sequences together, with meta
createHotkeys(() =>
  shortcuts.map((s) => ({
    hotkey: s.hotkey,
    callback: () => handleAction(s.id),
    options: {
      meta: { name: s.name, description: s.description },
    },
  })),
)

// Read all registrations reactively
const registrations = getHotkeyRegistrations()
// registrations.hotkeys[0].options.meta?.name → 'Save'
// registrations.hotkeys[0].triggerCount → 3`}</pre>
    </section>
  </main>
</div>
<script lang="ts">
  import {
    createHotkeyRecorder,
    createHotkeys,
    formatForDisplay,
    getHotkeyRegistrations,
  } from '@tanstack/svelte-hotkeys'
  import type { Hotkey } from '@tanstack/svelte-hotkeys'
  import ShortcutListItem from './ShortcutListItem.svelte'

  interface Shortcut {
    id: string
    name: string
    description: string
    /** A chord or a sequence; null when unassigned */
    hotkey: Hotkey | null
  }

  let nextId = 0
  function createId(): string {
    return `shortcut_${++nextId}`
  }

  let shortcuts = $state<Array<Shortcut>>([
    {
      id: createId(),
      name: 'Save',
      description: 'Save the current document',
      hotkey: 'Mod+S',
    },
    {
      id: createId(),
      name: 'Open (gg)',
      description: 'Open the file browser',
      hotkey: ['G', 'G'],
    },
    {
      id: createId(),
      name: 'New (dd)',
      description: 'Create a new document',
      hotkey: ['D', 'D'],
    },
    {
      id: createId(),
      name: 'Close',
      description: 'Close the current tab',
      hotkey: 'Mod+Shift+K',
    },
    {
      id: createId(),
      name: 'Undo (yy)',
      description: 'Undo the last action',
      hotkey: ['Y', 'Y'],
    },
    {
      id: createId(),
      name: 'Redo',
      description: 'Redo the last undone action',
      hotkey: 'Mod+Shift+G',
    },
  ])

  let editingId = $state<string | null>(null)
  let draftName = $state('')
  let draftDescription = $state('')

  const recorder = createHotkeyRecorder({
    // Record up to four chords; Enter or the fourth chord commits.
    maxSteps: 4,
    onRecord: (hotkey: Hotkey) => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                hotkey,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
    onCancel: () => {
      if (editingId) {
        const shortcut = shortcuts.find((s) => s.id === editingId)
        if (shortcut && shortcut.hotkey === null) {
          shortcuts = shortcuts.filter((s) => s.id !== editingId)
        }
      }
      editingId = null
    },
    onClear: () => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                hotkey: null,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
  })

  let isRecording = $derived(recorder.isRecording)

  // Register every assigned shortcut, chords and sequences alike, with meta
  createHotkeys(() =>
    shortcuts.flatMap((s) =>
      s.hotkey === null
        ? []
        : [
            {
              hotkey: s.hotkey,
              callback: () => {
                console.log(`${s.name} triggered:`, s.hotkey)
              },
              options: {
                meta: { name: s.name, description: s.description },
              },
            },
          ],
    ),
  )

  function handleEdit(id: string) {
    const shortcut = shortcuts.find((s) => s.id === id)
    if (!shortcut) return
    editingId = id
    draftName = shortcut.name
    draftDescription = shortcut.description
    recorder.startRecording()
  }

  function handleSaveEditing() {
    if (editingId) {
      shortcuts = shortcuts.map((s) =>
        s.id === editingId
          ? { ...s, name: draftName, description: draftDescription }
          : s,
      )
      recorder.stopRecording()
      editingId = null
    }
  }

  function handleCancel() {
    recorder.cancelRecording()
  }

  function handleDelete(id: string) {
    shortcuts = shortcuts.filter((s) => s.id !== id)
  }

  function handleCreateNew() {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      hotkey: null,
    }
    shortcuts = [...shortcuts, newShortcut]
    editingId = newShortcut.id
    draftName = ''
    draftDescription = ''
    recorder.startRecording()
  }

  // Registrations viewer
  const registrations = getHotkeyRegistrations()
</script>

<div class="app">
  <header>
    <h1>Recording Sequences</h1>
    <p>
      <code>createHotkeyRecorder</code> with <code>maxSteps</code> records up to that
      many chords. Click Edit, press each chord in order, then press Enter (or reach
      four chords) to save. One chord saves as a regular hotkey. Escape cancels; Backspace
      removes the last chord or clears when empty.
    </p>
  </header>

  <main>
    <section class="demo-section">
      <h2>Shortcuts</h2>
      <div class="shortcuts-list">
        {#each shortcuts as shortcut (shortcut.id)}
          <ShortcutListItem
            {shortcut}
            isEditing={editingId === shortcut.id}
            draftName={editingId === shortcut.id ? draftName : shortcut.name}
            draftDescription={editingId === shortcut.id
              ? draftDescription
              : shortcut.description}
            onDraftNameChange={(v) => (draftName = v)}
            onDraftDescriptionChange={(v) => (draftDescription = v)}
            liveSteps={recorder.steps}
            onEdit={() => handleEdit(shortcut.id)}
            onSave={handleSaveEditing}
            onCancel={handleCancel}
            onDelete={() => handleDelete(shortcut.id)}
          />
        {/each}
      </div>
      <button
        type="button"
        class="create-button"
        onclick={handleCreateNew}
        disabled={isRecording}
      >
        + Create New Shortcut
      </button>
    </section>

    {#if recorder.isRecording}
      <div class="info-box recording-notice">
        <strong>Recording...</strong> Press up to four chords, then Enter to
        finish. Escape cancels. Backspace removes the last chord or clears.
        {#if recorder.steps.length > 0}
          <div>
            Steps:
            <kbd>{formatForDisplay(recorder.steps)}</kbd>
          </div>
        {/if}
      </div>
    {/if}

    <!-- Registrations Viewer -->
    <section class="demo-section">
      <h2>Live Registrations</h2>
      <p>
        This table is powered by <code>getHotkeyRegistrations()</code> — trigger counts,
        names, and descriptions update in real-time as you use your shortcuts.
      </p>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Hotkey</th>
            <th>Name</th>
            <th>Description</th>
            <th>Progress</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          {#each registrations.hotkeys as reg (reg.id)}
            <tr>
              <td>
                <kbd>{formatForDisplay(reg.hotkey)}</kbd>
              </td>
              <td>{reg.options.meta?.name ?? '—'}</td>
              <td class="description-cell">
                {reg.options.meta?.description ?? '—'}
              </td>
              <td>
                {reg.kind === 'sequence'
                  ? `${reg.matchedStepCount}/${reg.hotkey.length}`
                  : '—'}
              </td>
              <td>
                <span
                  class={reg.options.enabled !== false
                    ? 'status-on'
                    : 'status-off'}
                >
                  {reg.options.enabled !== false ? 'yes' : 'no'}
                </span>
              </td>
              <td class="trigger-count">{reg.triggerCount}</td>
            </tr>
          {/each}
          {#if registrations.hotkeys.length === 0}
            <tr>
              <td colspan="6" class="hint">No hotkeys registered</td>
            </tr>
          {/if}
        </tbody>
      </table>
    </section>

    <section class="demo-section">
      <h2>Usage</h2>
      <pre class="code-block">{`import {
  createHotkeys,
  createHotkeyRecorder,
  getHotkeyRegistrations,
} from '@tanstack/svelte-hotkeys'

// Record a chord or a sequence of up to four chords
const recorder = createHotkeyRecorder({
  maxSteps: 4,
  onRecord: (hotkey) => save(hotkey), // 'Mod+S' or ['G', 'G']
})

// Register chords and sequences together, with meta
createHotkeys(() =>
  shortcuts.map((s) => ({
    hotkey: s.hotkey,
    callback: () => handleAction(s.id),
    options: {
      meta: { name: s.name, description: s.description },
    },
  })),
)

// Read all registrations reactively
const registrations = getHotkeyRegistrations()
// registrations.hotkeys[0].options.meta?.name → 'Save'
// registrations.hotkeys[0].triggerCount → 3`}</pre>
    </section>
  </main>
</div>