Skip to content

Navigation & Shortcuts ​

Keyboard shortcuts ​

Modifier + key ​

ts
// In a command definition (display hint only — use perform() for the action)
{
  id: 'save',
  label: 'Save File',
  shortcut: ['$mod', 's'],
  perform: () => save(),
}

To bind a real global shortcut, use createKeyboardManager() directly:

ts
import { createKeyboardManager } from '@macrulez/vue-command-palette'

const km = createKeyboardManager()
km.start()

const unregister = km.registerShortcut(['$mod', 'shift', 'p'], () => {
  openCommandPalette()
})

// Later, to clean up:
unregister()
km.stop()

registerShortcut ​

(keys: string[], handler: () => void) => () => void

Registers a shortcut and returns an unregister function. Plain-key and shift-only shortcuts are ignored while typing in an editable field (<input>, <textarea>, contenteditable) — $mod/Ctrl/Meta combos still fire so the palette can always be opened.

start ​

() => void

Attaches the keydown listener. No-op on the server or if already listening.

stop ​

() => void

Removes the keydown listener.

Bare-key sequences ​

Two consecutive keys without any modifier, within a 500 ms window:

ts
km.registerShortcut(['g', 'h'], () => router.push('/home'))
km.registerShortcut(['g', 'p'], () => router.push('/projects'))
km.registerShortcut(['g', 's'], () => router.push('/settings'))

Key reference ​

StringResolved to
'$mod'Meta on macOS, Ctrl on Windows/Linux
'shift'Shift
'alt'Alt / Option
'ctrl'Ctrl (explicit, not cross-platform)
'meta'Meta / Cmd (explicit)
Any other stringCompared with event.key.toLowerCase()

Binding shortcuts ​

By default shortcut is a display-only hint. Set bindShortcuts: true and each command's shortcut becomes a real global hotkey that runs the command through the same flow as clicking it (confirm dialogs and pages included) — this applies to shortcuts on commands nested inside subCommands at any depth too, not just top-level ones. Shortcuts are registered and cleaned up automatically as commands are added and removed.

ts
app.use(VCommandPalettePlugin, { bindShortcuts: true })

useRegisterCommands([
  { id: 'save', label: 'Save', shortcut: ['$mod', 's'], perform: () => save() },
  { id: 'find', label: 'Find', shortcut: ['$mod', 'f'], perform: () => openFind() },
])

Nested palettes ​

Add subCommands to any command to open a child palette when it is selected. The parent state is pushed to a breadcrumb history stack.

ts
{
  id: 'change-theme',
  label: 'Change Theme',
  icon: '🎨',
  perform: () => {},  // not called when subCommands is present
  subCommands: [
    {
      id: 'theme-light',
      label: 'Light',
      icon: '☀️',
      enabled: () => theme.value !== 'light',
      perform: () => { theme.value = 'light' },
    },
    {
      id: 'theme-dark',
      label: 'Dark',
      icon: '🌙',
      enabled: () => theme.value !== 'dark',
      perform: () => { theme.value = 'dark' },
    },
    {
      id: 'theme-system',
      label: 'System',
      icon: '💻',
      enabled: () => theme.value !== 'system',
      perform: () => { theme.value = 'system' },
    },
  ],
}

Sub-palettes can be nested to any depth.

Nested commands are searchable ​

By default (searchNested: true), typing a query also matches commands inside subCommands, so searching light surfaces the actual Light command — not just its Change Theme parent. Such results are shown with a breadcrumb context (Change Theme › Light) via SearchResult.parents, and selecting one runs it directly. Commands that open a sub-palette or page show a › chevron affordance. Set searchNested: false to restrict search to top-level commands only.

Navigation keys inside a sub-palette:

KeyAction
Backspace (empty input)Go back to parent palette
EscGo back if history exists, otherwise close

Command pages ​

A command can open a page instead of (or in addition to) running. A page is like a nested palette, but with its own placeholder and an async onSearch handler driven by the input — ideal for remote pickers and filters.

ts
useRegisterCommands([
  {
    id: 'assign-user',
    label: 'Assign to user…',
    icon: '👤',
    perform: () => {}, // not called — the page opens instead
    page: {
      placeholder: 'Search users…',
      // items: [...]   // optional static items shown on empty query
      onSearch: async (query) => {
        const users = await api.searchUsers(query)
        return users.map((u) => ({
          id: `user-${u.id}`,
          label: u.name,
          description: u.email,
          perform: () => assign(u.id),
        }))
      },
    },
  },
])

Backspace (empty input) / Esc navigate back, exactly like sub-palettes. Results are debounced 200 ms; if onSearch is omitted, the page filters its static items by the query.

Confirmation step ​

Set confirm to a non-empty string to require user confirmation before the command runs.

ts
{
  id: 'delete-project',
  label: 'Delete Project',
  icon: '🗑️',
  keywords: ['remove', 'erase'],
  confirm: 'Delete this project permanently? This action cannot be undone.',
  perform: async () => {
    await api.deleteProject(projectId)
    router.push('/')
  },
}

The palette replaces the result list with the confirmation message and two buttons:

  • Yes, proceed — executes the command and closes
  • Cancel — dismisses and returns to the result list

Enter confirms, Esc cancels.

Query history ​

Recently submitted queries are remembered for the session. With an empty or any input, press Alt+ArrowUp / Alt+ArrowDown to cycle through previous queries (most recent first). Exposed read-only as useCommandPalette().queryHistory.