Vue Example: UseHotkeySequences

<script setup lang="ts">
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
  HotkeysProvider,
  formatForDisplay,
  useHotkey,
  useHotkeyRegistrations,
  useHotkeySequences,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import { ref } from 'vue'

const lastSequence = ref<string | null>(null)
const history = ref<Array<string>>([])
const helloSequenceEnabled = ref(true)
const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]

const addToHistory = (action: string) => {
  lastSequence.value = action
  history.value = [...history.value.slice(-9), action]
}

useHotkeySequences([
  {
    sequence: ['G', 'G'],
    callback: () => addToHistory('gg → Go to top'),
    options: {
      meta: {
        name: 'Go to top',
        description: 'Scroll to the beginning of the document',
      },
    },
  },
  {
    sequence: ['Shift+G'],
    callback: () => addToHistory('G → Go to bottom'),
    options: {
      meta: {
        name: 'Go to bottom',
        description: 'Scroll to the end of the document',
      },
    },
  },
  {
    sequence: ['D', 'D'],
    callback: () => addToHistory('dd → Delete line'),
    options: {
      meta: { name: 'Delete line', description: 'Delete the current line' },
    },
  },
  {
    sequence: ['Y', 'Y'],
    callback: () => addToHistory('yy → Yank (copy) line'),
    options: {
      meta: {
        name: 'Yank line',
        description: 'Copy the current line to clipboard',
      },
    },
  },
  {
    sequence: ['D', 'W'],
    callback: () => addToHistory('dw → Delete word'),
    options: {
      meta: {
        name: 'Delete word',
        description: 'Delete from cursor to end of word',
      },
    },
  },
  {
    sequence: ['C', 'I', 'W'],
    callback: () => addToHistory('ciw → Change inner word'),
    options: {
      meta: {
        name: 'Change inner word',
        description: 'Delete word under cursor and enter insert mode',
      },
    },
  },
  {
    sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
    callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
    options: {
      timeout: 1500,
      meta: {
        name: 'Konami code',
        description: 'Partial Konami code using arrow keys',
      },
    },
  },
  {
    sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
    callback: () => addToHistory('←→←→ → Side to side!'),
    options: {
      timeout: 1500,
      meta: {
        name: 'Side to side',
        description: 'Left-right-left-right arrow pattern',
      },
    },
  },
  {
    sequence: ['H', 'E', 'L', 'L', 'O'],
    callback: () => addToHistory('hello → Hello World!'),
    options: () => ({
      enabled: helloSequenceEnabled.value,
      meta: { name: 'Hello', description: 'Spell out hello to trigger' },
    }),
  },
  {
    sequence: ['Shift+R', 'Shift+T'],
    callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
    options: {
      meta: {
        name: 'Chained Shift',
        description: 'Two consecutive Shift+letter chords',
      },
    },
  },
])

useHotkey('Escape', () => {
  lastSequence.value = null
  history.value = []
})

// Registrations viewer
const { hotkeys: registeredHotkeys, sequences: registeredSequences } =
  useHotkeyRegistrations()

const usageCode = `import { useHotkeySequences, useHotkeyRegistrations } from '@tanstack/vue-hotkeys'

useHotkeySequences([
  {
    sequence: ['G', 'G'],
    callback: () => scrollToTop(),
    options: { meta: { name: 'Go to top', description: 'Scroll to top' } },
  },
  {
    sequence: ['C', 'I', 'W'],
    callback: () => changeInnerWord(),
    options: { meta: { name: 'Change inner word' } },
  },
])

// Introspect all registrations
const { sequences } = useHotkeyRegistrations()
// sequences.value[0].options.meta?.name → 'Go to top'
// sequences.value[0].triggerCount → 3`
</script>

<template>
  <HotkeysProvider>
    <div class="app">
      <header>
        <h1>useHotkeySequences</h1>
        <p>
          Register many multi-key sequences in one composable (like Vim
          commands). Keys must be pressed within the timeout window (default:
          1000ms).
        </p>
      </header>

      <main>
        <section class="demo-section">
          <h2>Vim-Style Commands</h2>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td><kbd>g</kbd> <kbd>g</kbd></td>
                <td>Go to top</td>
              </tr>
              <tr>
                <td><kbd>G</kbd> (Shift+G)</td>
                <td>Go to bottom</td>
              </tr>
              <tr>
                <td><kbd>d</kbd> <kbd>d</kbd></td>
                <td>Delete line</td>
              </tr>
              <tr>
                <td><kbd>y</kbd> <kbd>y</kbd></td>
                <td>Yank (copy) line</td>
              </tr>
              <tr>
                <td><kbd>d</kbd> <kbd>w</kbd></td>
                <td>Delete word</td>
              </tr>
              <tr>
                <td><kbd>c</kbd> <kbd>i</kbd> <kbd>w</kbd></td>
                <td>Change inner word</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Fun Sequences</h2>
          <div class="fun-sequences">
            <div class="sequence-card">
              <h3>Konami Code (Partial)</h3>
              <p><kbd>↑</kbd> <kbd>↑</kbd> <kbd>↓</kbd> <kbd>↓</kbd></p>
              <span class="hint">Use arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Side to Side</h3>
              <p><kbd>←</kbd> <kbd>→</kbd> <kbd>←</kbd> <kbd>→</kbd></p>
              <span class="hint">Arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Spell It Out</h3>
              <p>
                <kbd>h</kbd> <kbd>e</kbd> <kbd>l</kbd> <kbd>l</kbd> <kbd>o</kbd>
              </p>
              <span class="hint">Type "hello" quickly</span>
              <p class="sequence-toggle-status">
                This sequence is
                <strong>{{
                  helloSequenceEnabled ? 'enabled' : 'disabled'
                }}</strong
                >.
              </p>
              <button
                type="button"
                @click="helloSequenceEnabled = !helloSequenceEnabled"
              >
                {{ helloSequenceEnabled ? 'Disable' : 'Enable' }} sequence
              </button>
            </div>
          </div>
        </section>

        <div v-if="lastSequence" class="info-box success">
          <strong>Triggered:</strong> {{ lastSequence }}
        </div>

        <section class="demo-section">
          <h2>Input handling</h2>
          <p>
            Sequences are not detected when typing in text inputs, textareas,
            selects, or contenteditable elements. Button-type inputs (
            <code>type="button"</code>, <code>submit</code>, <code>reset</code>)
            still receive sequences. Focus the input below and try <kbd>g</kbd>
            <kbd>g</kbd> or <kbd>h</kbd><kbd>e</kbd><kbd>l</kbd><kbd>l</kbd
            ><kbd>o</kbd> — nothing will trigger. Click outside to try again.
          </p>
          <input
            type="text"
            class="demo-input"
            placeholder="Focus here – sequences won't trigger while typing..."
          />
        </section>

        <section class="demo-section">
          <h2>Chained Shift+letter sequences</h2>
          <p>
            Each step is a chord: hold <kbd>Shift</kbd> and press a letter. You
            can press <kbd>Shift</kbd> alone between steps—those modifier-only
            presses do not reset progress, so the next chord still counts.
          </p>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>Shift</kbd>+<kbd>r</kbd> then <kbd>Shift</kbd>+
                  <kbd>t</kbd>
                </td>
                <td>Chained Shift+letter (2 steps)</td>
              </tr>
            </tbody>
          </table>
        </section>

        <!-- Live Registrations Viewer -->
        <section class="demo-section">
          <h2>Live Registrations (useHotkeyRegistrations)</h2>
          <p>
            This table is rendered from
            <code>useHotkeyRegistrations()</code> — a reactive view of all
            registered hotkeys and sequences. Trigger counts update in
            real-time.
          </p>

          <template v-if="registeredHotkeys.length > 0">
            <h3>Hotkeys</h3>
            <table class="registrations-table">
              <thead>
                <tr>
                  <th>Hotkey</th>
                  <th>Name</th>
                  <th>Description</th>
                  <th>Triggers</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="reg in registeredHotkeys" :key="reg.id">
                  <td>
                    <kbd>{{ formatForDisplay(reg.hotkey) }}</kbd>
                  </td>
                  <td>{{ reg.options.meta?.name ?? '\u2014' }}</td>
                  <td class="description-cell">
                    {{ reg.options.meta?.description ?? '\u2014' }}
                  </td>
                  <td class="trigger-count">{{ reg.triggerCount }}</td>
                </tr>
              </tbody>
            </table>
          </template>

          <h3>Sequences ({{ registeredSequences.length }})</h3>
          <table class="registrations-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Name</th>
                <th>Description</th>
                <th>Enabled</th>
                <th>Triggers</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="reg in registeredSequences" :key="reg.id">
                <td>
                  <template v-for="(s, i) in reg.sequence" :key="i">
                    {{ i > 0 ? ' ' : '' }}<kbd>{{ formatForDisplay(s) }}</kbd>
                  </template>
                </td>
                <td>{{ reg.options.meta?.name ?? '\u2014' }}</td>
                <td class="description-cell">
                  {{ reg.options.meta?.description ?? '\u2014' }}
                </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>
              <tr v-if="registeredSequences.length === 0">
                <td colspan="5" class="hint">No sequences registered</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Usage</h2>
          <pre class="code-block">{{ usageCode }}</pre>
        </section>

        <section v-if="history.length > 0" class="demo-section">
          <h2>History</h2>
          <ul class="history-list">
            <li v-for="(item, index) in history" :key="index">{{ item }}</li>
          </ul>
          <button type="button" @click="history = []">Clear History</button>
        </section>

        <p class="hint">Press <kbd>Escape</kbd> to clear history</p>
      </main>

      <TanStackDevtools :plugins="plugins" />
    </div>
  </HotkeysProvider>
</template>
<script setup lang="ts">
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
  HotkeysProvider,
  formatForDisplay,
  useHotkey,
  useHotkeyRegistrations,
  useHotkeySequences,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import { ref } from 'vue'

const lastSequence = ref<string | null>(null)
const history = ref<Array<string>>([])
const helloSequenceEnabled = ref(true)
const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]

const addToHistory = (action: string) => {
  lastSequence.value = action
  history.value = [...history.value.slice(-9), action]
}

useHotkeySequences([
  {
    sequence: ['G', 'G'],
    callback: () => addToHistory('gg → Go to top'),
    options: {
      meta: {
        name: 'Go to top',
        description: 'Scroll to the beginning of the document',
      },
    },
  },
  {
    sequence: ['Shift+G'],
    callback: () => addToHistory('G → Go to bottom'),
    options: {
      meta: {
        name: 'Go to bottom',
        description: 'Scroll to the end of the document',
      },
    },
  },
  {
    sequence: ['D', 'D'],
    callback: () => addToHistory('dd → Delete line'),
    options: {
      meta: { name: 'Delete line', description: 'Delete the current line' },
    },
  },
  {
    sequence: ['Y', 'Y'],
    callback: () => addToHistory('yy → Yank (copy) line'),
    options: {
      meta: {
        name: 'Yank line',
        description: 'Copy the current line to clipboard',
      },
    },
  },
  {
    sequence: ['D', 'W'],
    callback: () => addToHistory('dw → Delete word'),
    options: {
      meta: {
        name: 'Delete word',
        description: 'Delete from cursor to end of word',
      },
    },
  },
  {
    sequence: ['C', 'I', 'W'],
    callback: () => addToHistory('ciw → Change inner word'),
    options: {
      meta: {
        name: 'Change inner word',
        description: 'Delete word under cursor and enter insert mode',
      },
    },
  },
  {
    sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
    callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'),
    options: {
      timeout: 1500,
      meta: {
        name: 'Konami code',
        description: 'Partial Konami code using arrow keys',
      },
    },
  },
  {
    sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
    callback: () => addToHistory('←→←→ → Side to side!'),
    options: {
      timeout: 1500,
      meta: {
        name: 'Side to side',
        description: 'Left-right-left-right arrow pattern',
      },
    },
  },
  {
    sequence: ['H', 'E', 'L', 'L', 'O'],
    callback: () => addToHistory('hello → Hello World!'),
    options: () => ({
      enabled: helloSequenceEnabled.value,
      meta: { name: 'Hello', description: 'Spell out hello to trigger' },
    }),
  },
  {
    sequence: ['Shift+R', 'Shift+T'],
    callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'),
    options: {
      meta: {
        name: 'Chained Shift',
        description: 'Two consecutive Shift+letter chords',
      },
    },
  },
])

useHotkey('Escape', () => {
  lastSequence.value = null
  history.value = []
})

// Registrations viewer
const { hotkeys: registeredHotkeys, sequences: registeredSequences } =
  useHotkeyRegistrations()

const usageCode = `import { useHotkeySequences, useHotkeyRegistrations } from '@tanstack/vue-hotkeys'

useHotkeySequences([
  {
    sequence: ['G', 'G'],
    callback: () => scrollToTop(),
    options: { meta: { name: 'Go to top', description: 'Scroll to top' } },
  },
  {
    sequence: ['C', 'I', 'W'],
    callback: () => changeInnerWord(),
    options: { meta: { name: 'Change inner word' } },
  },
])

// Introspect all registrations
const { sequences } = useHotkeyRegistrations()
// sequences.value[0].options.meta?.name → 'Go to top'
// sequences.value[0].triggerCount → 3`
</script>

<template>
  <HotkeysProvider>
    <div class="app">
      <header>
        <h1>useHotkeySequences</h1>
        <p>
          Register many multi-key sequences in one composable (like Vim
          commands). Keys must be pressed within the timeout window (default:
          1000ms).
        </p>
      </header>

      <main>
        <section class="demo-section">
          <h2>Vim-Style Commands</h2>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td><kbd>g</kbd> <kbd>g</kbd></td>
                <td>Go to top</td>
              </tr>
              <tr>
                <td><kbd>G</kbd> (Shift+G)</td>
                <td>Go to bottom</td>
              </tr>
              <tr>
                <td><kbd>d</kbd> <kbd>d</kbd></td>
                <td>Delete line</td>
              </tr>
              <tr>
                <td><kbd>y</kbd> <kbd>y</kbd></td>
                <td>Yank (copy) line</td>
              </tr>
              <tr>
                <td><kbd>d</kbd> <kbd>w</kbd></td>
                <td>Delete word</td>
              </tr>
              <tr>
                <td><kbd>c</kbd> <kbd>i</kbd> <kbd>w</kbd></td>
                <td>Change inner word</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Fun Sequences</h2>
          <div class="fun-sequences">
            <div class="sequence-card">
              <h3>Konami Code (Partial)</h3>
              <p><kbd></kbd> <kbd></kbd> <kbd></kbd> <kbd></kbd></p>
              <span class="hint">Use arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Side to Side</h3>
              <p><kbd></kbd> <kbd></kbd> <kbd></kbd> <kbd></kbd></p>
              <span class="hint">Arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Spell It Out</h3>
              <p>
                <kbd>h</kbd> <kbd>e</kbd> <kbd>l</kbd> <kbd>l</kbd> <kbd>o</kbd>
              </p>
              <span class="hint">Type "hello" quickly</span>
              <p class="sequence-toggle-status">
                This sequence is
                <strong>{{
                  helloSequenceEnabled ? 'enabled' : 'disabled'
                }}</strong
                >.
              </p>
              <button
                type="button"
                @click="helloSequenceEnabled = !helloSequenceEnabled"
              >
                {{ helloSequenceEnabled ? 'Disable' : 'Enable' }} sequence
              </button>
            </div>
          </div>
        </section>

        <div v-if="lastSequence" class="info-box success">
          <strong>Triggered:</strong> {{ lastSequence }}
        </div>

        <section class="demo-section">
          <h2>Input handling</h2>
          <p>
            Sequences are not detected when typing in text inputs, textareas,
            selects, or contenteditable elements. Button-type inputs (
            <code>type="button"</code>, <code>submit</code>, <code>reset</code>)
            still receive sequences. Focus the input below and try <kbd>g</kbd>
            <kbd>g</kbd> or <kbd>h</kbd><kbd>e</kbd><kbd>l</kbd><kbd>l</kbd
            ><kbd>o</kbd> — nothing will trigger. Click outside to try again.
          </p>
          <input
            type="text"
            class="demo-input"
            placeholder="Focus here – sequences won't trigger while typing..."
          />
        </section>

        <section class="demo-section">
          <h2>Chained Shift+letter sequences</h2>
          <p>
            Each step is a chord: hold <kbd>Shift</kbd> and press a letter. You
            can press <kbd>Shift</kbd> alone between steps—those modifier-only
            presses do not reset progress, so the next chord still counts.
          </p>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>Shift</kbd>+<kbd>r</kbd> then <kbd>Shift</kbd>+
                  <kbd>t</kbd>
                </td>
                <td>Chained Shift+letter (2 steps)</td>
              </tr>
            </tbody>
          </table>
        </section>

        <!-- Live Registrations Viewer -->
        <section class="demo-section">
          <h2>Live Registrations (useHotkeyRegistrations)</h2>
          <p>
            This table is rendered from
            <code>useHotkeyRegistrations()</code> — a reactive view of all
            registered hotkeys and sequences. Trigger counts update in
            real-time.
          </p>

          <template v-if="registeredHotkeys.length > 0">
            <h3>Hotkeys</h3>
            <table class="registrations-table">
              <thead>
                <tr>
                  <th>Hotkey</th>
                  <th>Name</th>
                  <th>Description</th>
                  <th>Triggers</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="reg in registeredHotkeys" :key="reg.id">
                  <td>
                    <kbd>{{ formatForDisplay(reg.hotkey) }}</kbd>
                  </td>
                  <td>{{ reg.options.meta?.name ?? '\u2014' }}</td>
                  <td class="description-cell">
                    {{ reg.options.meta?.description ?? '\u2014' }}
                  </td>
                  <td class="trigger-count">{{ reg.triggerCount }}</td>
                </tr>
              </tbody>
            </table>
          </template>

          <h3>Sequences ({{ registeredSequences.length }})</h3>
          <table class="registrations-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Name</th>
                <th>Description</th>
                <th>Enabled</th>
                <th>Triggers</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="reg in registeredSequences" :key="reg.id">
                <td>
                  <template v-for="(s, i) in reg.sequence" :key="i">
                    {{ i > 0 ? ' ' : '' }}<kbd>{{ formatForDisplay(s) }}</kbd>
                  </template>
                </td>
                <td>{{ reg.options.meta?.name ?? '\u2014' }}</td>
                <td class="description-cell">
                  {{ reg.options.meta?.description ?? '\u2014' }}
                </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>
              <tr v-if="registeredSequences.length === 0">
                <td colspan="5" class="hint">No sequences registered</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Usage</h2>
          <pre class="code-block">{{ usageCode }}</pre>
        </section>

        <section v-if="history.length > 0" class="demo-section">
          <h2>History</h2>
          <ul class="history-list">
            <li v-for="(item, index) in history" :key="index">{{ item }}</li>
          </ul>
          <button type="button" @click="history = []">Clear History</button>
        </section>

        <p class="hint">Press <kbd>Escape</kbd> to clear history</p>
      </main>

      <TanStackDevtools :plugins="plugins" />
    </div>
  </HotkeysProvider>
</template>