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