Reference
Command type
interface Command<T = unknown> {
id: string // unique identifier
label: string // display text, searched by fuzzy engine
description?: string // subtitle shown below the label
icon?: Component | string // Vue component or emoji / string
keywords?: string[] // extra search terms
aliases?: string[] // alternate labels (same score as label match)
shortcut?: string[] // display-only hint: ['$mod', 'k']
disabled?: boolean // permanently unavailable
enabled?: () => boolean // dynamically disable — evaluated on each render
disabledReason?: string // tooltip shown when the command is disabled
badge?: string | { text: string; color?: string } // small label (e.g. "New", "Pro")
confirm?: string // prompt shown before execute
perform: () => void | Promise<void> // action; may be async
subCommands?: Command[] // opens a nested palette when selected
page?: CommandPage // opens a page with its own input/async search
actions?: CommandAction[] // secondary actions, opened with Tab
info?: string // text/HTML shown in the preview pane (v-html)
data?: T // type-safe payload (see Typed command data)
}
interface CommandAction {
id: string
label: string
icon?: Component | string
shortcut?: string[] // display-only hint
perform: () => void | Promise<void>
}
interface CommandPage {
placeholder?: string // input placeholder on the page
items?: Command[] // static items (empty query)
onSearch?: (query: string) => Command[] | Promise<Command[]> // query-driven results (debounced)
}icon field
Accepts an emoji string, a plain text string, or any Vue component:
import MyIcon from './MyIcon.vue'
{
icon: '🏠'
} // emoji string
{
icon: '⌘'
} // symbol string
{
icon: MyIcon
} // Vue component — rendered as <MyIcon />Typed command data
Attach an arbitrary, type-safe payload to commands via the generic Command<T> and its data field. useRegisterCommands<T> / useRegisterGroup<T>, fuzzySearch<T>, SearchResult<T> and SearchFn<T> all carry the type through, so you get full inference (and errors on mismatches). It defaults to unknown, so existing untyped usage is unaffected.
interface UserData {
id: number
email: string
}
useRegisterCommands<UserData>([
{
id: 'user-ada',
label: 'Ada Lovelace',
data: { id: 1, email: 'ada@example.com' }, // checked against UserData
perform: () => {},
},
])
// Standalone search keeps the type:
const results = fuzzySearch<UserData>('ada', commands)
results[0].command.data?.email // string | undefined// @ts-expect-error — data must match UserData
const bad: Command<UserData> = { id: 'x', label: 'X', data: { wrong: true }, perform: () => {} }Inside the
#item/#previewslots thecommandis typed asCommand(data: unknown) since the palette stores commands of mixed types — narrow with a cast or a type guard when you need the payload there.
TypeScript types
All public types are exported from the package root:
import type {
Command,
CommandGroupType, // group definition — NOT the CommandGroup component
CommandAction, // secondary action on a command
CommandPage, // page opened by a command (placeholder + async onSearch)
SearchResult, // { command, score, matches, groupId?, parents?, matchedField? }
SearchFn, // custom search strategy signature
PaletteMode, // prefix-activated scope
CommandUsage, // frecency stat { count, lastUsed }
PaletteOptions,
PaletteLabels, // customisable UI strings (i18n)
PaletteContext,
PaletteState,
CommandStore,
KeyboardManager,
} from '@macrulez/vue-command-palette'Note: The named export
CommandGroupis the Vue component. The group-definition interface is exported asCommandGroupTypeto avoid the collision.
SearchResult
interface SearchResult {
command: Command
score: number
matches: Array<[start: number, end: number]>
groupId?: string
parents?: Command[] // ancestor chain when the result is a nested sub-command
matchedField?: 'label' | 'description' | 'keyword' | 'alias' // which field won the score
matchedText?: string // matching keyword/alias text
}PaletteContext
The full injectable context, accessible in custom composables via inject(PALETTE_INJECT_KEY):
interface PaletteContext {
store: CommandStore
keyboard: KeyboardManager
isOpen: Ref<boolean>
query: Ref<string>
activeIndex: Ref<number>
history: Ref<HistoryEntry[]>
recentIds: Ref<string[]>
loadingCommandId: Ref<string | null>
results: ComputedRef<SearchResult[]>
persistRecent: boolean
maxRecent: number
maxRecentPerGroup: number
localStorageKey: string
onOpen?: () => void
onClose?: () => void
onError?: (err: unknown, command: Command) => void
}Accessibility
| Feature | Implementation |
|---|---|
role="dialog" + aria-modal="true" | Applied to the palette dialog element |
role="combobox" | Applied to the search <input> |
aria-expanded="true" | Set on the input while the palette is open |
aria-controls | Input points to the role="listbox" result list |
aria-activedescendant | Updated as the keyboard-active item changes |
role="listbox" | Applied to the result list container |
role="option" | Applied to each CommandItem |
aria-selected | Set to true on the currently active item |
aria-disabled | Set when disabled: true or enabled() returns false |
aria-live="polite" | Breadcrumb — screen readers announce sub-palette navigation; a visually-hidden region also announces the result count (labels.resultsCount) |
| Focus trap | Tab is intercepted to keep focus inside the dialog |
| Scroll lock | document.body.style.overflow is set to hidden while open |
| Reduced motion | @media (prefers-reduced-motion: reduce) disables the fade transition |
SSR compatibility
All browser-only APIs are guarded before use:
// KeyboardManager — skips addEventListener on the server
if (typeof document === 'undefined') return
// Recent commands — skips localStorage on the server
if (typeof localStorage === 'undefined') return
// CommandItem — platform detection for ⌘ vs Ctrl label
typeof navigator !== 'undefined' && navigator.platform.includes('Mac')VirtualList (used for result sets > 50 items) renders an empty placeholder on the server and hydrates on the client. All slot content and command registration are fully SSR-safe.
Bundle size
| Entry point | Peer deps | Notes |
|---|---|---|
@macrulez/vue-command-palette | vue ^3.3 | Components, composables, fuzzy engine, keyboard manager |
@macrulez/vue-command-palette/style.css | — | Default styles; ~3 KB |
@macrulez/vue-command-palette/testing | vue ^3.3 | createPaletteContext + PaletteProvider; dev/test only |
@macrulez/vue-command-palette/nuxt | nuxt ^3, vue ^3.3 | Nuxt auto-plugin |
Ships as tree-shakeable ESM (dist/@macrulez/vue-command-palette.js) + CJS (dist/@macrulez/vue-command-palette.cjs). Core bundle without styles is ~11 KB gzip; the stylesheet is ~2 KB gzip.
License
MIT