Skip to content

useShortcuts ​

Binds keyboard shortcuts. Ctrl+K is simultaneous; g d is a sequence. Both register themselves in the cheat sheet.

The example below binds through bindShortcuts and releases on unmount, because it is running inside a page you will navigate away from. useShortcuts itself binds for the life of the app.

Open in demo

Import ​

ts
import {
  bindShortcuts,
  registerShortcutMeta,
  unregisterShortcutMeta,
  useShortcutCheatSheet,
  useShortcutRegistry,
  useShortcuts,
} from '@shared/composables/useShortcuts'

Signature ​

ts
function useShortcuts(shortcuts: ShortcutMap, meta?: Record<string, ShortcutMeta>): void

type ShortcutMap = Record<string, () => void>

interface ShortcutMeta {
  description: string
  group?: string
}

Combo syntax ​

FormExampleMeans
+ between keys'Ctrl+K'Pressed together.
Spaces between keys'g d'g, then d, within 800ms.

VueUse's magic-keys has no native sequences, so those are detected here.

Example ​

vue
<script setup lang="ts">
import { useShortcuts } from '@shared/composables/useShortcuts'
import { useRouter } from 'vue-router'

const router = useRouter()

useShortcuts(
  {
    'g i': () => { void router.push('/invoices') },
    'Ctrl+Shift+N': () => { void router.push('/invoices/new') },
  },
  {
    'g i': { description: 'Go to invoices', group: 'Navigation' },
    'Ctrl+Shift+N': { description: 'New invoice', group: 'Actions' },
  },
)
</script>

<template>
  <p>Press <kbd>g</kbd> then <kbd>i</kbd>.</p>
</template>

Notes ​

  • A bare-key shortcut is suppressed while focus is in an input, a textarea, a select or anything contenteditable, so ? does not open the cheat sheet in the middle of a sentence. A combo with a modifier always fires.
  • The same combo can be bound twice, and only the newest one runs. Each binding takes a token and pushes it onto a stack per combo; the handler checks it owns the top of that stack. Without it, a page binding g s would fire its own handler and the global one on the same keystroke.
  • A sequence resets if the wrong key is pressed, or after 800ms of nothing.
  • useShortcuts binds for the life of the app and never cleans up. For a page, use usePageShortcuts, which tears down on scope dispose.

bindShortcuts ​

The core both wrappers use. It returns an array of cleanup functions, so you can own the teardown yourself:

ts
import { bindShortcuts } from '@shared/composables/useShortcuts'

const cleanups = bindShortcuts({ 'g h': () => console.log('home') })

for (const stop of cleanups)
  stop()

useShortcutRegistry ​

ts
function useShortcutRegistry(): {
  shortcuts: Readonly<Ref<RegisteredShortcut[]>>
}

RegisteredShortcut is { combo: string, description: string, group: string }. group defaults to common.general.

Every registration is listed, in the order it was made, and a combo can appear more than once. The cheat sheet shows the last registration for each combo, which is what that key does right now — the same rule the binding itself follows.

useShortcutCheatSheet ​

ts
function useShortcutCheatSheet(): {
  open: Ref<boolean>
}

Singleton open state, so the navbar button, the ? key and the host in App.vue all drive one ref.

What is bound already ​

ComboDoes
?Opens the cheat sheet.
g dDashboard.
g sSettings.
g pProfile.
t sToggles the sidebar.
t cToggles the theme customizer.
Ctrl+KOpens the command palette.

g goes somewhere, t toggles a piece of chrome.

Ctrl+K is a plain listener, not a useShortcuts binding. It needs preventDefault() to beat the browser's own search box, and it has to work before the lazily loaded palette chunk arrives. It is registered in the cheat sheet by hand with registerShortcutMeta, and it reads the commandPalette feature flag inside the handler, so switching that off in Settings takes effect on the next keystroke rather than the next reload.