Vue Example: UseHotkeys

<script setup lang="ts">
import { computed, ref } from 'vue'
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
  HotkeysProvider,
  formatForDisplay,
  useHotkeyRegistrations,
  useHotkeys,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import type { Hotkey, UseHotkeyDefinition } from '@tanstack/vue-hotkeys'

const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]

// Basic demo
const log = ref<Array<string>>([])
const saveCount = ref(0)
const undoCount = ref(0)
const redoCount = ref(0)

useHotkeys([
  {
    hotkey: 'Shift+S',
    callback: (_e, { hotkey }) => {
      saveCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Save', description: 'Save the current document' },
    },
  },
  {
    hotkey: 'Shift+U',
    callback: (_e, { hotkey }) => {
      undoCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Undo', description: 'Undo the last action' },
    },
  },
  {
    hotkey: 'Shift+R',
    callback: (_e, { hotkey }) => {
      redoCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Redo', description: 'Redo the last undone action' },
    },
  },
])

// Common options demo
const commonEnabled = ref(true)
const counts = ref({ a: 0, b: 0, c: 0 })

useHotkeys(
  [
    {
      hotkey: 'Alt+J',
      callback: () => {
        counts.value = { ...counts.value, a: counts.value.a + 1 }
      },
      options: {
        meta: {
          name: 'Action A',
          description: 'First action (respects toggle)',
        },
      },
    },
    {
      hotkey: 'Alt+K',
      callback: () => {
        counts.value = { ...counts.value, b: counts.value.b + 1 }
      },
      options: {
        meta: {
          name: 'Action B',
          description: 'Second action (respects toggle)',
        },
      },
    },
    {
      hotkey: 'Alt+L',
      callback: () => {
        counts.value = { ...counts.value, c: counts.value.c + 1 }
      },
      options: {
        enabled: true,
        meta: {
          name: 'Action C',
          description: 'Always-on action (overrides toggle)',
        },
      },
    },
  ],
  () => ({ enabled: commonEnabled.value }),
)

// Dynamic demo
interface DynamicShortcut {
  id: number
  hotkey: string
  label: string
  description: string
  count: number
}

let nextId = 0

const shortcuts = ref<Array<DynamicShortcut>>([
  {
    id: nextId++,
    hotkey: 'Shift+A',
    label: 'Action A',
    description: 'First dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+B',
    label: 'Action B',
    description: 'Second dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+C',
    label: 'Action C',
    description: 'Third dynamic action',
    count: 0,
  },
])

const newHotkey = ref('')
const newLabel = ref('')
const newDescription = ref('')

const dynamicDefinitions = computed<Array<UseHotkeyDefinition>>(() =>
  shortcuts.value.map((s) => ({
    hotkey: s.hotkey as Hotkey,
    callback: () => {
      shortcuts.value = shortcuts.value.map((item) =>
        item.id === s.id ? { ...item, count: item.count + 1 } : item,
      )
    },
    options: {
      meta: { name: s.label, description: s.description },
    },
  })),
)

useHotkeys(dynamicDefinitions)

function addShortcut() {
  const trimmed = newHotkey.value.trim()
  if (!trimmed || !newLabel.value.trim()) return
  shortcuts.value = [
    ...shortcuts.value,
    {
      id: nextId++,
      hotkey: trimmed,
      label: newLabel.value.trim(),
      description: newDescription.value.trim(),
      count: 0,
    },
  ]
  newHotkey.value = ''
  newLabel.value = ''
  newDescription.value = ''
}

function removeShortcut(id: number) {
  shortcuts.value = shortcuts.value.filter((s) => s.id !== id)
}

function fd(h: string) {
  return formatForDisplay(h as Hotkey)
}

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

<template>
  <HotkeysProvider>
    <div class="app">
      <header>
        <h1>useHotkeys</h1>
        <p>
          Register multiple hotkeys in a single composable call. Supports
          dynamic arrays for variable-length shortcut lists.
        </p>
      </header>

      <!-- Basic Multi-Hotkey -->
      <div class="demo-section">
        <h2>Basic Multi-Hotkey Registration</h2>
        <p>
          All three hotkeys are registered in a single
          <code>useHotkeys()</code> call with <code>meta</code> for name and
          description.
        </p>
        <div class="hotkey-grid">
          <div>
            <kbd>{{ fd('Shift+S') }}</kbd> Save ({{ saveCount }})
          </div>
          <div>
            <kbd>{{ fd('Shift+U') }}</kbd> Undo ({{ undoCount }})
          </div>
          <div>
            <kbd>{{ fd('Shift+R') }}</kbd> Redo ({{ redoCount }})
          </div>
        </div>
        <div v-if="log.length > 0" class="log">
          <div v-for="(entry, i) in log" :key="i" class="log-entry">
            {{ entry }}
          </div>
        </div>
        <pre class="code-block">
useHotkeys([
  {
    hotkey: 'Shift+S',
    callback: () => save(),
    options: { meta: { name: 'Save', description: 'Save the document' } },
  },
  {
    hotkey: 'Shift+U',
    callback: () => undo(),
    options: { meta: { name: 'Undo', description: 'Undo the last action' } },
  },
])</pre
        >
      </div>

      <!-- Common Options -->
      <div class="demo-section">
        <h2>Common Options with Per-Hotkey Overrides</h2>
        <p>
          <kbd>{{ fd('Alt+J') }}</kbd> and <kbd>{{ fd('Alt+K') }}</kbd> respect
          the global toggle. <kbd>{{ fd('Alt+L') }}</kbd> overrides
          <code>enabled: true</code> so it always works.
        </p>
        <div style="margin-bottom: 12px">
          <button @click="commonEnabled = !commonEnabled">
            {{ commonEnabled ? 'Disable' : 'Enable' }} common hotkeys
          </button>
        </div>
        <div class="hotkey-grid">
          <div>
            <kbd>{{ fd('Alt+J') }}</kbd> Action A ({{ counts.a }})
          </div>
          <div>
            <kbd>{{ fd('Alt+K') }}</kbd> Action B ({{ counts.b }})
          </div>
          <div>
            <kbd>{{ fd('Alt+L') }}</kbd> Action C ({{ counts.c }})
            <span class="hint"> (always on)</span>
          </div>
        </div>
        <pre class="code-block">
useHotkeys(
  [
    { hotkey: 'Alt+J', callback: () => actionA(),
      options: { meta: { name: 'Action A' } } },
    { hotkey: 'Alt+L', callback: () => actionC(),
      options: { enabled: true, meta: { name: 'Action C' } } },
  ],
  { enabled }, // common option
)</pre
        >
      </div>

      <!-- Dynamic -->
      <div class="demo-section">
        <h2>Dynamic Hotkey List</h2>
        <p>
          Add or remove hotkeys at runtime. Because <code>useHotkeys</code>
          accepts a dynamic array, this works without any special handling.
        </p>
        <div class="dynamic-list">
          <div v-for="s in shortcuts" :key="s.id" class="dynamic-item">
            <kbd>{{ fd(s.hotkey) }}</kbd>
            <span>{{ s.label }}</span>
            <span class="count">{{ s.count }}</span>
            <button @click="removeShortcut(s.id)">Remove</button>
          </div>
          <p v-if="shortcuts.length === 0" class="hint">
            No shortcuts registered. Add one below.
          </p>
        </div>
        <div class="add-form">
          <input
            v-model="newHotkey"
            type="text"
            placeholder="Hotkey (e.g. Shift+D)"
            @keydown.enter="addShortcut"
          />
          <input
            v-model="newLabel"
            type="text"
            placeholder="Name (e.g. Action D)"
            @keydown.enter="addShortcut"
          />
          <input
            v-model="newDescription"
            type="text"
            placeholder="Description (optional)"
            @keydown.enter="addShortcut"
          />
          <button :disabled="!newHotkey || !newLabel" @click="addShortcut">
            Add
          </button>
        </div>
        <pre class="code-block">
const shortcuts = useShortcutsConfig() // dynamic data

useHotkeys(
  shortcuts.map((s) => ({
    hotkey: s.key,
    callback: s.action,
    options: { meta: { name: s.name, description: s.description } },
  })),
)</pre
        >
      </div>

      <!-- Live Registrations Viewer -->
      <div class="demo-section">
        <h2>Live Registrations (useHotkeyRegistrations)</h2>
        <p>
          This table is rendered from
          <code>useHotkeyRegistrations()</code> — a reactive view of all
          registered hotkeys. It updates automatically as hotkeys are added,
          removed, enabled/disabled, or triggered.
        </p>
        <table class="registrations-table">
          <thead>
            <tr>
              <th>Hotkey</th>
              <th>Name</th>
              <th>Description</th>
              <th>Enabled</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>
                <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="registeredHotkeys.length === 0">
              <td colspan="5" class="hint">No hotkeys registered</td>
            </tr>
          </tbody>
        </table>
        <template v-if="registeredSequences.length > 0">
          <h3 style="margin-top: 16px">Sequences</h3>
          <table class="registrations-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Name</th>
                <th>Description</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 class="trigger-count">{{ reg.triggerCount }}</td>
              </tr>
            </tbody>
          </table>
        </template>
        <pre class="code-block">
const { hotkeys, sequences } = useHotkeyRegistrations()

// Render a live table of all registrations
hotkeys.value.map((reg) => ({
  hotkey: formatForDisplay(reg.hotkey),
  name: reg.options.meta?.name,
  description: reg.options.meta?.description,
  triggers: reg.triggerCount,
}))</pre
        >
      </div>
    </div>

    <TanStackDevtools :plugins="plugins" />
  </HotkeysProvider>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
  HotkeysProvider,
  formatForDisplay,
  useHotkeyRegistrations,
  useHotkeys,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import type { Hotkey, UseHotkeyDefinition } from '@tanstack/vue-hotkeys'

const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]

// Basic demo
const log = ref<Array<string>>([])
const saveCount = ref(0)
const undoCount = ref(0)
const redoCount = ref(0)

useHotkeys([
  {
    hotkey: 'Shift+S',
    callback: (_e, { hotkey }) => {
      saveCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Save', description: 'Save the current document' },
    },
  },
  {
    hotkey: 'Shift+U',
    callback: (_e, { hotkey }) => {
      undoCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Undo', description: 'Undo the last action' },
    },
  },
  {
    hotkey: 'Shift+R',
    callback: (_e, { hotkey }) => {
      redoCount.value++
      log.value = [`${hotkey} pressed`, ...log.value].slice(0, 20)
    },
    options: {
      meta: { name: 'Redo', description: 'Redo the last undone action' },
    },
  },
])

// Common options demo
const commonEnabled = ref(true)
const counts = ref({ a: 0, b: 0, c: 0 })

useHotkeys(
  [
    {
      hotkey: 'Alt+J',
      callback: () => {
        counts.value = { ...counts.value, a: counts.value.a + 1 }
      },
      options: {
        meta: {
          name: 'Action A',
          description: 'First action (respects toggle)',
        },
      },
    },
    {
      hotkey: 'Alt+K',
      callback: () => {
        counts.value = { ...counts.value, b: counts.value.b + 1 }
      },
      options: {
        meta: {
          name: 'Action B',
          description: 'Second action (respects toggle)',
        },
      },
    },
    {
      hotkey: 'Alt+L',
      callback: () => {
        counts.value = { ...counts.value, c: counts.value.c + 1 }
      },
      options: {
        enabled: true,
        meta: {
          name: 'Action C',
          description: 'Always-on action (overrides toggle)',
        },
      },
    },
  ],
  () => ({ enabled: commonEnabled.value }),
)

// Dynamic demo
interface DynamicShortcut {
  id: number
  hotkey: string
  label: string
  description: string
  count: number
}

let nextId = 0

const shortcuts = ref<Array<DynamicShortcut>>([
  {
    id: nextId++,
    hotkey: 'Shift+A',
    label: 'Action A',
    description: 'First dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+B',
    label: 'Action B',
    description: 'Second dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+C',
    label: 'Action C',
    description: 'Third dynamic action',
    count: 0,
  },
])

const newHotkey = ref('')
const newLabel = ref('')
const newDescription = ref('')

const dynamicDefinitions = computed<Array<UseHotkeyDefinition>>(() =>
  shortcuts.value.map((s) => ({
    hotkey: s.hotkey as Hotkey,
    callback: () => {
      shortcuts.value = shortcuts.value.map((item) =>
        item.id === s.id ? { ...item, count: item.count + 1 } : item,
      )
    },
    options: {
      meta: { name: s.label, description: s.description },
    },
  })),
)

useHotkeys(dynamicDefinitions)

function addShortcut() {
  const trimmed = newHotkey.value.trim()
  if (!trimmed || !newLabel.value.trim()) return
  shortcuts.value = [
    ...shortcuts.value,
    {
      id: nextId++,
      hotkey: trimmed,
      label: newLabel.value.trim(),
      description: newDescription.value.trim(),
      count: 0,
    },
  ]
  newHotkey.value = ''
  newLabel.value = ''
  newDescription.value = ''
}

function removeShortcut(id: number) {
  shortcuts.value = shortcuts.value.filter((s) => s.id !== id)
}

function fd(h: string) {
  return formatForDisplay(h as Hotkey)
}

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

<template>
  <HotkeysProvider>
    <div class="app">
      <header>
        <h1>useHotkeys</h1>
        <p>
          Register multiple hotkeys in a single composable call. Supports
          dynamic arrays for variable-length shortcut lists.
        </p>
      </header>

      <!-- Basic Multi-Hotkey -->
      <div class="demo-section">
        <h2>Basic Multi-Hotkey Registration</h2>
        <p>
          All three hotkeys are registered in a single
          <code>useHotkeys()</code> call with <code>meta</code> for name and
          description.
        </p>
        <div class="hotkey-grid">
          <div>
            <kbd>{{ fd('Shift+S') }}</kbd> Save ({{ saveCount }})
          </div>
          <div>
            <kbd>{{ fd('Shift+U') }}</kbd> Undo ({{ undoCount }})
          </div>
          <div>
            <kbd>{{ fd('Shift+R') }}</kbd> Redo ({{ redoCount }})
          </div>
        </div>
        <div v-if="log.length > 0" class="log">
          <div v-for="(entry, i) in log" :key="i" class="log-entry">
            {{ entry }}
          </div>
        </div>
        <pre class="code-block">
useHotkeys([
  {
    hotkey: 'Shift+S',
    callback: () => save(),
    options: { meta: { name: 'Save', description: 'Save the document' } },
  },
  {
    hotkey: 'Shift+U',
    callback: () => undo(),
    options: { meta: { name: 'Undo', description: 'Undo the last action' } },
  },
])</pre
        >
      </div>

      <!-- Common Options -->
      <div class="demo-section">
        <h2>Common Options with Per-Hotkey Overrides</h2>
        <p>
          <kbd>{{ fd('Alt+J') }}</kbd> and <kbd>{{ fd('Alt+K') }}</kbd> respect
          the global toggle. <kbd>{{ fd('Alt+L') }}</kbd> overrides
          <code>enabled: true</code> so it always works.
        </p>
        <div style="margin-bottom: 12px">
          <button @click="commonEnabled = !commonEnabled">
            {{ commonEnabled ? 'Disable' : 'Enable' }} common hotkeys
          </button>
        </div>
        <div class="hotkey-grid">
          <div>
            <kbd>{{ fd('Alt+J') }}</kbd> Action A ({{ counts.a }})
          </div>
          <div>
            <kbd>{{ fd('Alt+K') }}</kbd> Action B ({{ counts.b }})
          </div>
          <div>
            <kbd>{{ fd('Alt+L') }}</kbd> Action C ({{ counts.c }})
            <span class="hint"> (always on)</span>
          </div>
        </div>
        <pre class="code-block">
useHotkeys(
  [
    { hotkey: 'Alt+J', callback: () => actionA(),
      options: { meta: { name: 'Action A' } } },
    { hotkey: 'Alt+L', callback: () => actionC(),
      options: { enabled: true, meta: { name: 'Action C' } } },
  ],
  { enabled }, // common option
)</pre
        >
      </div>

      <!-- Dynamic -->
      <div class="demo-section">
        <h2>Dynamic Hotkey List</h2>
        <p>
          Add or remove hotkeys at runtime. Because <code>useHotkeys</code>
          accepts a dynamic array, this works without any special handling.
        </p>
        <div class="dynamic-list">
          <div v-for="s in shortcuts" :key="s.id" class="dynamic-item">
            <kbd>{{ fd(s.hotkey) }}</kbd>
            <span>{{ s.label }}</span>
            <span class="count">{{ s.count }}</span>
            <button @click="removeShortcut(s.id)">Remove</button>
          </div>
          <p v-if="shortcuts.length === 0" class="hint">
            No shortcuts registered. Add one below.
          </p>
        </div>
        <div class="add-form">
          <input
            v-model="newHotkey"
            type="text"
            placeholder="Hotkey (e.g. Shift+D)"
            @keydown.enter="addShortcut"
          />
          <input
            v-model="newLabel"
            type="text"
            placeholder="Name (e.g. Action D)"
            @keydown.enter="addShortcut"
          />
          <input
            v-model="newDescription"
            type="text"
            placeholder="Description (optional)"
            @keydown.enter="addShortcut"
          />
          <button :disabled="!newHotkey || !newLabel" @click="addShortcut">
            Add
          </button>
        </div>
        <pre class="code-block">
const shortcuts = useShortcutsConfig() // dynamic data

useHotkeys(
  shortcuts.map((s) => ({
    hotkey: s.key,
    callback: s.action,
    options: { meta: { name: s.name, description: s.description } },
  })),
)</pre
        >
      </div>

      <!-- Live Registrations Viewer -->
      <div class="demo-section">
        <h2>Live Registrations (useHotkeyRegistrations)</h2>
        <p>
          This table is rendered from
          <code>useHotkeyRegistrations()</code> — a reactive view of all
          registered hotkeys. It updates automatically as hotkeys are added,
          removed, enabled/disabled, or triggered.
        </p>
        <table class="registrations-table">
          <thead>
            <tr>
              <th>Hotkey</th>
              <th>Name</th>
              <th>Description</th>
              <th>Enabled</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>
                <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="registeredHotkeys.length === 0">
              <td colspan="5" class="hint">No hotkeys registered</td>
            </tr>
          </tbody>
        </table>
        <template v-if="registeredSequences.length > 0">
          <h3 style="margin-top: 16px">Sequences</h3>
          <table class="registrations-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Name</th>
                <th>Description</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 class="trigger-count">{{ reg.triggerCount }}</td>
              </tr>
            </tbody>
          </table>
        </template>
        <pre class="code-block">
const { hotkeys, sequences } = useHotkeyRegistrations()

// Render a live table of all registrations
hotkeys.value.map((reg) => ({
  hotkey: formatForDisplay(reg.hotkey),
  name: reg.options.meta?.name,
  description: reg.options.meta?.description,
  triggers: reg.triggerCount,
}))</pre
        >
      </div>
    </div>

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