Skip to content

Reference

Command type

ts
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:

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

ts
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
// @ts-expect-error — data must match UserData
const bad: Command<UserData> = { id: 'x', label: 'X', data: { wrong: true }, perform: () => {} }

Inside the #item / #preview slots the command is typed as Command (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:

ts
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 CommandGroup is the Vue component. The group-definition interface is exported as CommandGroupType to avoid the collision.

SearchResult

ts
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):

ts
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

FeatureImplementation
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-controlsInput points to the role="listbox" result list
aria-activedescendantUpdated as the keyboard-active item changes
role="listbox"Applied to the result list container
role="option"Applied to each CommandItem
aria-selectedSet to true on the currently active item
aria-disabledSet 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 trapTab is intercepted to keep focus inside the dialog
Scroll lockdocument.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:

ts
// 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 pointPeer depsNotes
@macrulez/vue-command-palettevue ^3.3Components, composables, fuzzy engine, keyboard manager
@macrulez/vue-command-palette/style.cssDefault styles; ~3 KB
@macrulez/vue-command-palette/testingvue ^3.3createPaletteContext + PaletteProvider; dev/test only
@macrulez/vue-command-palette/nuxtnuxt ^3, vue ^3.3Nuxt 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