Skip to main content
You are viewing Pre-Alpha documentation.
Vuetify0 Logo
Theme
Mode
Accessibility
Vuetify

Sign in

Sign in with your preferred provider to access your account.

useHotkey

A composable for handling hotkey combinations and sequences with platform-aware modifiers and automatic cleanup.


Intermediate98.1% coverageFeb 4, 2026

Usage

The useHotkey composable registers hotkey handlers on the window with automatic cleanup when the component is unmounted. It supports key combinations (ctrl+k), key sequences (g-h), and platform-aware modifier mapping.

UseHotkey
<script setup lang="ts">
  import { useHotkey } from '@vuetify/v0'
  import { ref } from 'vue'

  const commandPaletteOpen = ref(false)
  const message = ref('')

  // Simple key combination - Ctrl+K (Cmd+K on Mac)
  useHotkey('ctrl+k', () => {
    commandPaletteOpen.value = true
  })

  // Key sequence (GitHub-style) - press 'g' then 'h'
  useHotkey('g-h', () => {
    message.value = 'Go home!'
  })

  // With multiple modifiers
  useHotkey('ctrl+shift+s', () => {
    message.value = 'Save all!'
  })

  // Allow in inputs with the inputs option
  useHotkey('escape', () => {
    commandPaletteOpen.value = false
  }, { inputs: true })
</script>

<template>
  <div>
    <p>Press Ctrl+K to open command palette</p>
    <p>Press g then h for GitHub-style navigation</p>
    <p v-if="message">{{ message }}</p>
  </div>
</template>

Examples

ESC
navigate selectesc close
<script setup lang="ts">
  import { Dialog, useHotkey, useToggleScope } from '@vuetify/v0'
  import { IN_BROWSER } from '@vuetify/v0/constants'
  import { computed, shallowRef, useTemplateRef, watch } from 'vue'

  const isOpen = shallowRef(false)
  const query = shallowRef('')
  const selectedIndex = shallowRef(0)
  const inputRef = useTemplateRef('input')

  const isMac = IN_BROWSER && navigator.userAgent.includes('Mac')
  const modKey = isMac ? '⌘' : 'Ctrl'

  const lastAction = shallowRef('')

  const commands = [
    { id: 'theme', label: 'Toggle theme', icon: 'i-mdi-brightness-6', hotkey: 'L', action: () => lastAction.value = 'Toggled theme' },
    { id: 'search', label: 'Search docs', icon: 'i-mdi-magnify', hotkey: 'S', action: () => lastAction.value = 'Opened search' },
    { id: 'home', label: 'Go to home', icon: 'i-mdi-home', hotkey: 'H', action: () => lastAction.value = 'Navigated home' },
    { id: 'github', label: 'View on GitHub', icon: 'i-mdi-github', hotkey: 'G', action: () => lastAction.value = 'Opened GitHub' },
    { id: 'copy', label: 'Copy link', icon: 'i-mdi-link', hotkey: 'C', action: () => lastAction.value = 'Copied link' },
    { id: 'feedback', label: 'Send feedback', icon: 'i-mdi-message-outline', hotkey: 'F', action: () => lastAction.value = 'Opened feedback' },
  ]

  const filtered = computed(() =>
    commands.filter(c => c.label.toLowerCase().includes(query.value.toLowerCase())),
  )

  // Global hotkey to open palette
  useHotkey('cmd+j', () => {
    isOpen.value = true
  })

  // Hotkeys active only when palette is open
  useToggleScope(isOpen, () => {
    // Command shortcuts (inputs: true to work while typing in search)
    for (const cmd of commands) {
      useHotkey(`cmd+${cmd.hotkey.toLowerCase()}`, () => {
        cmd.action()
        isOpen.value = false
      }, { inputs: true })
    }

    // Navigation
    useHotkey('arrowdown', () => {
      selectedIndex.value = (selectedIndex.value + 1) % filtered.value.length
    }, { inputs: true })

    useHotkey('arrowup', () => {
      selectedIndex.value = (selectedIndex.value - 1 + filtered.value.length) % filtered.value.length
    }, { inputs: true })
  })

  watch(isOpen, open => {
    if (open) {
      query.value = ''
      selectedIndex.value = 0
      setTimeout(() => inputRef.value?.focus(), 0)
    }
  })

  watch(filtered, () => {
    selectedIndex.value = 0
  })
</script>

<template>
  <div class="text-center">
    <Dialog.Root v-model="isOpen">
      <Dialog.Activator class="inline-flex items-center gap-3 px-4 py-2 bg-surface border border-divider rounded-lg hover:border-primary transition-colors">
        <span class="i-mdi-magnify text-on-surface-variant" />
        <span class="text-on-surface-variant">Search commands...</span>
        <kbd class="ml-4 px-1.5 py-0.5 text-xs font-mono bg-surface-variant text-on-surface-variant rounded">
          {{ modKey }}+J
        </kbd>
      </Dialog.Activator>

      <p v-if="lastAction" class="mt-3 text-sm text-on-surface-variant">
        Last action: <span class="text-primary font-medium">{{ lastAction }}</span>
      </p>

      <Dialog.Content class="w-full max-w-lg mx-auto mt-[15vh] rounded-xl bg-surface border border-divider shadow-2xl overflow-hidden text-left">
        <div class="flex items-center gap-3 px-4 py-3 border-b border-divider">
          <span class="i-mdi-magnify text-xl text-on-surface-variant" />
          <input
            ref="input"
            v-model="query"
            class="flex-1 bg-transparent text-on-surface outline-none placeholder:text-on-surface-variant"
            placeholder="Type a command..."
          >
          <kbd class="px-1.5 py-0.5 text-xs font-mono bg-surface-variant text-on-surface-variant rounded">ESC</kbd>
        </div>

        <div class="max-h-72 overflow-auto py-2">
          <Dialog.Close
            v-for="(cmd, i) in filtered"
            :key="cmd.id"
            as="button"
            class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors"
            :class="i === selectedIndex ? 'bg-primary/10 text-primary' : 'text-on-surface hover:bg-surface-variant'"
            @click="cmd.action()"
            @mouseenter="selectedIndex = i"
          >
            <span class="text-lg" :class="cmd.icon" />
            <span class="flex-1">{{ cmd.label }}</span>
            <kbd class="px-1.5 py-0.5 text-xs font-mono bg-surface-variant/50 rounded">
              {{ modKey }}+{{ cmd.hotkey }}
            </kbd>
          </Dialog.Close>
          <div v-if="filtered.length === 0" class="px-4 py-8 text-center text-on-surface-variant">
            No commands found
          </div>
        </div>

        <div class="flex items-center gap-4 px-4 py-2 border-t border-divider text-xs text-on-surface-variant">
          <span class="flex items-center gap-1"><kbd class="px-1 py-0.5 bg-surface-variant rounded">↑</kbd><kbd class="px-1 py-0.5 bg-surface-variant rounded">↓</kbd> navigate</span>
          <span class="flex items-center gap-1"><kbd class="px-1 py-0.5 bg-surface-variant rounded">↵</kbd> select</span>
          <span class="flex items-center gap-1"><kbd class="px-1 py-0.5 bg-surface-variant rounded">esc</kbd> close</span>
        </div>
      </Dialog.Content>
    </Dialog.Root>
  </div>
</template>

Architecture

useHotkey builds on useEventListener for keyboard event handling:

Hotkey Hierarchy

Use controls to zoom and pan. Click outside or press Escape to close.

Hotkey Hierarchy

Reactivity

Property/MethodReactiveNotes
isActiveComputed from cleanup ref
isPausedShallowRef, readonly
keysAccepts MaybeRefOrGetter, watched for changes

API Reference

The following API details are for the useHotkey composable.

Functions

useHotkey

(keys: MaybeRefOrGetter<string>, callback: (e: KeyboardEvent) => void, options?: UseHotkeyOptions, _platform?: PlatformContext) => UseHotkeyReturn

A composable that listens for hotkey combinations and sequences.

Options

event

MaybeRefOrGetter<"keydown" | "keyup">

The keyboard event type to listen for.

Default: 'keydown'

inputs

MaybeRefOrGetter<boolean>

Whether to trigger the callback when an input element is focused.

Default: false

preventDefault

MaybeRefOrGetter<boolean>

Whether to prevent the default browser action.

Default: true

stopPropagation

MaybeRefOrGetter<boolean>

Whether to stop event propagation.

Default: false

sequenceTimeout

MaybeRefOrGetter<number>

Timeout in ms before a key sequence resets.

Default: 1000

Properties

isActive

Readonly<Ref<boolean, boolean>>

Whether the hotkey listener is currently active (listening for keys). False when paused, when keys is undefined, or in SSR.

isPaused

Readonly<Ref<boolean, boolean>>

Whether the hotkey listener is currently paused.

Methods

pause

() => void

Pause listening (removes listener but keeps configuration).

resume

() => void

Resume listening after pause.

stop

() => void

Stop listening and clean up (removes listener).

Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/