Navigation & Shortcuts
Keyboard shortcuts
Modifier + key
// 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:
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:
km.registerShortcut(['g', 'h'], () => router.push('/home'))
km.registerShortcut(['g', 'p'], () => router.push('/projects'))
km.registerShortcut(['g', 's'], () => router.push('/settings'))Key reference
| String | Resolved 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 string | Compared 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.
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.
{
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:
| Key | Action |
|---|---|
Backspace (empty input) | Go back to parent palette |
Esc | Go 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.
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.
{
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.