Keyboard shortcut: Ctrl + K
Get started

Command palette

VCommandPalette opens a search field over a list of commands in a native modal dialog. Type to narrow the list, use the arrows to move and Enter to run a command. The shortcut prop opens it from anywhere in the page.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VCommandPalette, VHotkeys } from 'vectis-ui'
import type { CommandPaletteItem } from 'vectis-ui'
import { add, notifications, schedule, table_chart as tableChart } from 'vectis-ui/icons'

const open = ref(false)
const last = ref('')

const items: CommandPaletteItem[] = [
  { label: 'New document', icon: add },
  { label: 'Open reports', icon: tableChart },
  { label: 'Sync now', icon: schedule },
  { label: 'Notification settings', icon: notifications },
]
</script>

<template>
  <div class="demo">
    <VCommandPalette
      v-model:open="open"
      :items="items"
      shortcut="alt+k"
      @select="(command) => (last = command.label)"
    >
      <template #trigger="{ triggerProps }">
        <VButton v-bind="triggerProps" variant="outline" tone="neutral">
          Search commands
          <VHotkeys keys="alt+k" />
        </VButton>
      </template>
    </VCommandPalette>
    <p v-if="last">Ran: {{ last }}</p>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vectis-space-4);
}

p {
  margin: 0;
}
</style>

Examples

Groups

Pass commands, named groups and separators in items. keywords are matched without being shown, and the shortcut of a command is displayed only.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VCommandPalette } from 'vectis-ui'
import type { CommandPaletteItem } from 'vectis-ui'
import { add, info, notifications, schedule } from 'vectis-ui/icons'

const open = ref(false)
const last = ref('')

const items: CommandPaletteItem[] = [
  {
    label: 'Actions',
    commands: [
      {
        label: 'New document',
        description: 'A blank document in this workspace',
        icon: add,
        shortcut: 'mod+alt+n',
      },
      { label: 'Sync now', icon: schedule, keywords: ['refresh', 'reload'] },
      { label: 'Notification settings', icon: notifications },
    ],
  },
  { separator: true },
  {
    label: 'Help',
    commands: [{ label: 'Keyboard shortcuts', icon: info, shortcut: 'shift+/' }],
  },
]
</script>

<template>
  <div class="demo">
    <VCommandPalette
      v-model:open="open"
      :items="items"
      @select="(command) => (last = command.label)"
    >
      <template #trigger="{ triggerProps }">
        <VButton v-bind="triggerProps" variant="outline" tone="neutral">Open the palette</VButton>
      </template>
    </VCommandPalette>
    <p v-if="last">Ran: {{ last }}</p>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vectis-space-4);
}

p {
  margin: 0;
}
</style>

A command with href is a link: Enter follows it, and a modified click opens it in a new tab. With a router, call event.preventDefault() in select and navigate yourself.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VCommandPalette } from 'vectis-ui'
import type { CommandPaletteItem } from 'vectis-ui'
import { open_in_new as openInNew } from 'vectis-ui/icons'

const open = ref(false)

const items: CommandPaletteItem[] = [
  {
    label: 'Components',
    commands: [
      { label: 'Button', href: '#button' },
      { label: 'Dialog', href: '#dialog' },
      { label: 'Drawer', href: '#drawer' },
    ],
  },
  {
    label: 'Resources',
    commands: [
      { label: 'Vue documentation', href: 'https://vuejs.org', icon: openInNew },
      { label: 'Changelog', href: '#changelog', disabled: true },
    ],
  },
]
</script>

<template>
  <VCommandPalette v-model:open="open" :items="items" placeholder="Go to a page…">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps" variant="outline" tone="neutral">Go to</VButton>
    </template>
  </VCommandPalette>
</template>

Set filter to false and load the commands on search, which is debounced. loading shows a spinner in the field.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VCommandPalette } from 'vectis-ui'
import type { CommandPaletteCommand } from 'vectis-ui'
import { description } from 'vectis-ui/icons'

const DOCUMENTS = ['Roadmap 2026', 'Release plan', 'Planning poker notes', 'Team charter']

const open = ref(false)
const loading = ref(false)
const results = ref<CommandPaletteCommand[]>([])
let request = 0

function search(query: string) {
  const id = ++request
  loading.value = true
  // Stands in for a request to a server.
  setTimeout(() => {
    if (id !== request) return
    const q = query.toLowerCase()
    results.value = DOCUMENTS.filter((name) => name.toLowerCase().includes(q)).map((label) => ({
      label,
      icon: description,
    }))
    loading.value = false
  }, 400)
}
</script>

<template>
  <VCommandPalette
    v-model:open="open"
    :items="results"
    :filter="false"
    :loading="loading"
    loading-text="Searching documents…"
    placeholder="Search documents…"
    @search="search"
  >
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps">Search documents</VButton>
    </template>
  </VCommandPalette>
</template>

Recent commands

The palette keeps no history. Record choices from select and add a group of recent commands while v-model:query is empty.

vue
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { VButton, VCommandPalette } from 'vectis-ui'
import type { CommandPaletteCommand, CommandPaletteItem } from 'vectis-ui'
import { add, info, notifications, schedule } from 'vectis-ui/icons'

const STORAGE_KEY = 'recent-commands'

const COMMANDS: CommandPaletteCommand[] = [
  { id: 'new', label: 'New document', icon: add },
  { id: 'sync', label: 'Sync now', icon: schedule },
  { id: 'notifications', label: 'Notification settings', icon: notifications },
  { id: 'help', label: 'Keyboard shortcuts', icon: info },
]

const open = ref(false)
const query = ref('')
const recentIds = ref<string[]>([])

// Storage is read once mounted: it does not exist during server rendering.
onMounted(() => {
  try {
    recentIds.value = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]')
  } catch {
    recentIds.value = []
  }
})

const items = computed<CommandPaletteItem[]>(() => {
  const recent = recentIds.value
    .map((id) => COMMANDS.find((command) => command.id === id))
    .filter((command) => command !== undefined)
  if (query.value || recent.length === 0) return COMMANDS
  return [
    { label: 'Recent', commands: recent },
    { label: 'All commands', commands: COMMANDS },
  ]
})

function remember(command: CommandPaletteCommand) {
  const id = String(command.id)
  recentIds.value = [id, ...recentIds.value.filter((other) => other !== id)].slice(0, 3)
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(recentIds.value))
  } catch {
    // Storage may be unavailable, in a private window for instance.
  }
}
</script>

<template>
  <VCommandPalette v-model:open="open" v-model:query="query" :items="items" @select="remember">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps" variant="outline" tone="neutral">Search commands</VButton>
    </template>
  </VCommandPalette>
</template>

API

Props

PropTypeDefault
itemsCommandPaletteItem[]none
Commands, named groups and separators.
shortcutstringnone
Page-wide shortcut that toggles the palette, such as mod+k. Ignored while typing in another field. Adds aria-keyshortcuts to the trigger.
filterCommandPaletteFiltertrue
Built-in accent-insensitive filtering on labels and keywords, disabled filtering, or a custom function receiving the command and trimmed query.
searchDebouncenumber250
Delay in milliseconds before emitting a typed search. Zero emits immediately.
loadingbooleanfalse
Shows a field spinner, and a loading row while no command is available.
loadingTextstringnone
Loading text announced to screen readers. Keep it consistent with custom #loading content.
emptyTextstringnone
Title of the default VEmptyState, also announced to screen readers. Keep it consistent with custom #empty content.
placeholderstringnone
Placeholder of the search field. Defaults to the library dictionary.
labelstringnone
Accessible name of the palette and its list. Defaults to the library dictionary.
searchLabelstringnone
Accessible name of the search field. Defaults to the library dictionary.
hideFooterbooleanfalse
Hides the footer listing the keys.
widthnumber | stringnone
Palette width: pixels for numbers, otherwise a CSS length.
v-model:openbooleanfalse
Palette open state. Native dismissal updates the model.
v-model:querystring''
Search text. Emptied when the palette closes.

Events

EventType
search[query: string]
Emits the search term after searchDebounce, or immediately on opening. Consecutive duplicate terms are skipped.
select[command: CommandPaletteCommand, event: MouseEvent]
Emitted with the command and the click event when a command is chosen. The palette then closes unless the command has keepOpen.

Slots

SlotType
trigger{ triggerProps: CommandPaletteTriggerProps; }
Opening control. Bind the supplied triggerProps.
itemCommandPaletteItemSlotProps
Label and description of a row. Receives the command and its active state.
emptyCommandPaletteEmptySlotProps
Empty-state content. Receives the search query; also set emptyText.
loading{}
Loading content. Also set loadingText.
footer{}
Replaces the footer listing the keys.

Types

Types used in the API tables. Import exported types from vectis-ui.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export interface CommandPaletteCommand {
  label: string
  description?: string
  keywords?: string[]
  icon?: IconSource
  shortcut?: string
  href?: string
  disabled?: boolean
  keepOpen?: boolean
  id?: string | number
}
export interface CommandPaletteEmptySlotProps {
  query: string
}
export type CommandPaletteFilter =
  boolean | ((command: CommandPaletteCommand, query: string) => boolean)
export interface CommandPaletteGroup {
  label: string
  commands: CommandPaletteCommand[]
}
export type CommandPaletteItem =
  CommandPaletteCommand | CommandPaletteGroup | CommandPaletteSeparator
export interface CommandPaletteItemSlotProps {
  command: CommandPaletteCommand
  active: boolean
}
export interface CommandPaletteSeparator {
  separator: true
}
export type CommandPaletteTriggerProps = {
  onClick: () => void
  'aria-haspopup': 'dialog'
  'aria-keyshortcuts'?: string
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender

CSS variables

TokenValue
--vectis-control-size-command-palette-width40rem
--vectis-control-size-command-palette-list-max-block20rem
--vectis-control-size-command-palette-offset6rem