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()

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). 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.