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

Vue Example: UseHotkeys

vue
<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>