Command+K palette for Vue 3 — fuzzy search, grouped commands, nested sub-palettes, async search, and full headless customisation via slots.

Cmd+K has become an expected pattern in serious tools — vue-command-palette gives you a ready, fully customisable implementation instead of building search, keyboard navigation, and match highlighting from scratch.
Settings live in one corner, export in another, theme switching in a third — a command palette pulls all of it into one place, reachable from the keyboard without hunting through menus.
"Assign to user" can't just execute — it needs to find a person first; a nested page with its own async search handles that without a separate modal.
"Delete project" shouldn't be one click away like "Open settings" — a built-in confirm step guards against a stray click.
Instead of retyping them each time, recent and pinned commands rise to the top on an empty query, and the ones you use most often climb even higher over time.

Instant search with ranking (exact, prefix, substring, fuzzy), match highlighting, and diacritic normalisation. Group commands with headers, nested sub-palettes of any depth, and pages with their own async search.

Built‑in keyboard manager supports modifiers ($mod, shift, alt, ctrl, meta) and two‑key sequences. Bind shortcuts to commands or use them standalone. Commands with a shortcut auto‑register as global hotkeys.

Groups and pages can load commands on demand via onSearch with 200 ms debounce. Built‑in loading states, dynamic enabling/disabling via enabled(), async execution with a progress spinner, and error handling.

Command history persists in localStorage and shows on empty query. Pin favourite commands, and frecency (frequency + recency) boosts frequently used commands in search results. Query history is available via Alt+↑/↓.

Headless slots for full UI control, 20+ CSS variables for theming, built‑in light/dark theme. Ready‑to‑use hooks for Vue 3 and a Nuxt module. All APIs are SSR‑safe, and testing utilities simplify unit tests. The only dependency is Vue 3.

The results list renders through virtual scrolling, so even a palette with hundreds of commands stays smooth. Create independent palette instances via createCommandPalette(), and guard destructive commands with the built‑in confirmation step.
Two bare keys in a row, no modifier — g, then h — within a 500ms window. registerShortcut parses sequences like this itself.
import { createKeyboardManager } from '@macrulez/vue-command-palette'
const km = createKeyboardManager()
km.start()
km.registerShortcut(['g', 'h'], () => router.push('/home'))
km.registerShortcut(['g', 'p'], () => router.push('/projects'))
km.registerShortcut(['g', 's'], () => router.push('/settings'))
// Two bare keys, no modifier, pressed within a 500ms window — the same
// "g then h" navigation pattern as GitHub's own keyboard shortcuts.subCommands opens a child palette on selection, and a query like "light" finds the command inside it right away — with a "Change Theme › Light" breadcrumb, no need to open the submenu first.
const changeTheme = {
id: 'change-theme',
label: 'Change Theme',
icon: '🎨',
subCommands: [
{ id: 'theme-light', label: 'Light', icon: '☀️', perform: () => (theme.value = 'light') },
{ id: 'theme-dark', label: 'Dark', icon: '🌙', perform: () => (theme.value = 'dark') },
{ id: 'theme-system', label: 'System', icon: '💻', perform: () => (theme.value = 'system') },
],
}
// With searchNested (on by default), typing "light" finds the command
// inside the sub-palette directly — shown with a breadcrumb, "Change
// Theme › Light" — no need to open "Change Theme" first. A group's onSearch can hit any API — docs, a database, search — debounced 200ms, merged with the synchronous results, and re-sorted by relevance.
import { useRegisterGroup } from '@macrulez/vue-command-palette'
useRegisterGroup({
id: 'docs-search',
label: 'Documentation',
commands: [],
onSearch: async (query) => {
const results = await searchDocs(query)
return results.slice(0, 5).map((doc) => ({
id: `doc-${doc.slug}`,
label: doc.title,
description: doc.excerpt,
icon: '📄',
perform: () => window.open(doc.url, '_blank'),
}))
},
})
// Debounced 200ms, merged with the synchronous results and re-sorted by
// score — already-shown results stay visible while the request is in flight.