Examples
Live palettes from minimal to fully featured, each next to the exact file that runs it.
Each preview runs a single, self-contained component. Switch to the Code tab to read it. The same files run in the demo app under apps/example, so the two never drift apart.
The only global setup is the theme import from Getting started.
Minimal
Three commands and the dialog with its defaults, which is all a palette needs. The hotkey is off only so it doesn’t clash with the other palettes in the demo app, which already use ⌘K.
<script setup lang="ts">
import { ref } from 'vue'
import { CommandPaletteDialog, type Command } from 'vue-cmdk-palette'
const open = ref(false)
const last = ref('nothing yet')
const run = (label: string) => () => {
last.value = label
}
const commands: Command[] = [
{ kind: 'action', id: 'new-file', label: 'New file', perform: run('New file') },
{ kind: 'action', id: 'save', label: 'Save', shortcut: 'mod+s', perform: run('Save') },
{ kind: 'action', id: 'close', label: 'Close tab', perform: run('Close tab') },
]
</script>
<template>
<button type="button" class="button" @click="open = true">Open the minimal palette</button>
<p class="note">Last command: {{ last }}</p>
<!-- The page already owns ⌘K, so this one opens from the button only. -->
<CommandPaletteDialog v-model:open="open" :commands="commands" :hotkey="false" :recents="false" />
</template>
Compact
A dense inline palette for a toolbar or sidebar: smaller rows, no footer, and no quick-select digits. The density comes from two variables and three rules on the data-cmdk-* hooks.
<script setup lang="ts">
import { ref } from 'vue'
import { CommandPalette, type Command } from 'vue-cmdk-palette'
const last = ref('nothing yet')
const run = (label: string) => () => {
last.value = label
}
const commands: Command[] = [
{ kind: 'action', id: 'bold', label: 'Bold', group: 'Format', shortcut: 'mod+b', perform: run('Bold') },
{ kind: 'action', id: 'italic', label: 'Italic', group: 'Format', shortcut: 'mod+i', perform: run('Italic') },
{ kind: 'action', id: 'code', label: 'Inline code', group: 'Format', shortcut: 'mod+e', perform: run('Inline code') },
{ kind: 'action', id: 'h1', label: 'Heading 1', group: 'Blocks', keywords: ['title'], perform: run('Heading 1') },
{ kind: 'action', id: 'h2', label: 'Heading 2', group: 'Blocks', perform: run('Heading 2') },
{ kind: 'action', id: 'quote', label: 'Quote', group: 'Blocks', perform: run('Quote') },
{ kind: 'action', id: 'list', label: 'Bulleted list', group: 'Blocks', perform: run('Bulleted list') },
]
</script>
<template>
<div class="compact">
<CommandPalette
:commands="commands"
:recents="false"
:quick-select="false"
:autofocus="false"
:labels="{ placeholder: 'Insert or format…' }"
/>
</div>
<p class="note">Last command: {{ last }}</p>
</template>
<style scoped>
/* Density lives in a few variables plus three rules on the data-cmdk-* hooks. */
.compact {
--cmdk-radius: 8px;
--cmdk-row-radius: 5px;
max-width: 320px;
height: 260px;
display: flex;
flex-direction: column;
}
.compact :deep([data-cmdk-root]) {
flex: 1;
min-height: 0;
font-size: 13px;
}
.compact :deep([data-cmdk-item]) {
min-height: 28px;
padding: 3px 8px;
}
.compact :deep([data-cmdk-footer]) {
display: none;
}
</style>
Full
Most of what the library does, in one place:
- scope chips with
>and@prefixes - a nested page whose rows load from a promise
- links routed through
navigate, and a ⌘/Ctrl+Enter alternate action - a disabled row with a reason, and a hidden row that only search finds
- shortcuts, badges, hints and the detail panel
- recents in their own namespace
- the
selectanderrorevents
<script setup lang="ts">
import { ref } from 'vue'
import { CommandPaletteDialog, type Command, type Scope } from 'vue-cmdk-palette'
const open = ref(false)
const log = ref<string[]>([])
const note = (message: string): void => {
log.value = [message, ...log.value].slice(0, 4)
}
const scopes: Scope[] = [
{ id: 'pages', label: 'Pages' },
{ id: 'run', label: 'Run', prefix: '>' },
{ id: 'people', label: 'People', prefix: '@' },
]
const people = [
{ id: 'ada', name: 'Ada Lovelace', email: 'ada@example.com' },
{ id: 'grace', name: 'Grace Hopper', email: 'grace@example.com' },
{ id: 'alan', name: 'Alan Turing', email: 'alan@example.com' },
]
// Stands in for a request: the palette shows a loading row until it resolves.
const loadReleases = async (): Promise<Command[]> => {
await new Promise((resolve) => setTimeout(resolve, 600))
return ['2.1.0', '2.0.0', '1.4.2'].map((version) => ({
kind: 'link',
id: `release-${version}`,
label: `v${version}`,
href: `#/releases/${version}`,
}))
}
const commands: Command[] = [
{ kind: 'link', id: 'home', label: 'Home', href: '#/', group: 'Pages', scope: 'pages', alternateLabel: 'New tab' },
{
kind: 'link',
id: 'settings',
label: 'Settings',
href: '#/settings',
group: 'Pages',
scope: 'pages',
hint: 'Profile, billing and notifications',
keywords: ['preferences', 'account'],
},
{
kind: 'page',
id: 'releases',
label: 'Releases…',
group: 'Pages',
scope: 'pages',
hint: 'Loaded when you open it',
placeholder: 'Pick a release',
children: loadReleases,
},
{
kind: 'action',
id: 'copy-link',
label: 'Copy link',
group: 'Actions',
scope: 'run',
shortcut: 'mod+shift+c',
keywords: ['clipboard', 'share'],
perform: async () => {
await navigator.clipboard.writeText(location.href)
note('Copied the page URL')
},
},
{
kind: 'action',
id: 'export',
label: 'Export as PDF',
group: 'Actions',
scope: 'run',
badge: 'Pro',
disabled: 'Needs a Pro plan',
perform: () => undefined,
},
{
kind: 'action',
id: 'fail',
label: 'Run a failing task',
group: 'Actions',
scope: 'run',
hint: 'Throws, so you can see the error event',
perform: () => {
throw new Error('Task failed on purpose')
},
},
{
kind: 'action',
id: 'debug',
label: 'Toggle debug overlay',
group: 'Actions',
scope: 'run',
hidden: true,
hint: 'Hidden until you search for it',
perform: () => note('Debug overlay toggled'),
},
...people.map((person): Command => ({
kind: 'action',
id: `person-${person.id}`,
label: person.name,
group: 'People',
scope: 'people',
hint: person.email,
perform: () => note(`Opened ${person.name}`),
})),
]
const navigate = (href: string): void => note(`Navigated to ${href}`)
const onError = (error: unknown): void => note(`Error: ${error instanceof Error ? error.message : String(error)}`)
</script>
<template>
<button type="button" class="button" @click="open = true">Open the full palette</button>
<ol v-if="log.length" class="log">
<li v-for="(entry, index) in log" :key="index">{{ entry }}</li>
</ol>
<p v-else class="note">Try <code>>copy</code>, <code>@grace</code>, <code>debug</code> or Releases.</p>
<CommandPaletteDialog
v-model:open="open"
:commands="commands"
:scopes="scopes"
:group-order="['Pages', 'Actions', 'People']"
:navigate="navigate"
:hotkey="false"
:recents="{ namespace: 'full-example' }"
show-detail
@select="(command, mode) => note(`${mode === 'alternate' ? 'Alt-picked' : 'Picked'} ${command.label}`)"
@error="onError"
/>
</template>
Theme variables
A warm, serif theme made with --cmdk-* variables and nothing else. Variables are inherited, so you can set them on :root for the whole app or on a wrapper to theme one palette.
<script setup lang="ts">
import { ref } from 'vue'
import { CommandPalette, type Command, type CommandContext } from 'vue-cmdk-palette'
const last = ref('nothing yet')
const run = (context: CommandContext): void => {
last.value = context.command.label
}
const commands: Command[] = [
{ kind: 'action', id: 'deploy', label: 'Deploy to production', group: 'Project', badge: 'Live', perform: run },
{ kind: 'action', id: 'preview', label: 'Create a preview', group: 'Project', shortcut: 'mod+p', perform: run },
{ kind: 'action', id: 'logs', label: 'Open logs', group: 'Project', hint: 'Last 24 hours', perform: run },
{ kind: 'action', id: 'invite', label: 'Invite a teammate', group: 'Team', perform: run },
]
</script>
<template>
<!-- Variables are inherited, so setting them on any ancestor themes everything inside. -->
<div class="brand">
<CommandPalette :commands="commands" :recents="false" :autofocus="false" />
</div>
<p class="note">Last command: {{ last }}</p>
</template>
<style scoped>
.brand {
--cmdk-font: Georgia, 'Times New Roman', serif;
--cmdk-radius: 18px;
--cmdk-row-radius: 999px;
--cmdk-bg: #fdf6ec;
--cmdk-bg-sunken: #f6ead8;
--cmdk-bg-hover: #f3e3cc;
--cmdk-fg: #3b2414;
--cmdk-fg-muted: #7a5a43;
--cmdk-fg-faint: #a3836b;
--cmdk-border: #ead6bb;
--cmdk-border-strong: #dcc09c;
--cmdk-accent: #c2410c;
--cmdk-accent-fg: #ffffff;
--cmdk-badge-bg: #c2410c;
--cmdk-badge-fg: #ffffff;
height: 300px;
display: flex;
flex-direction: column;
}
.brand :deep([data-cmdk-root]) {
flex: 1;
min-height: 0;
}
</style>
Custom CSS
When variables aren’t enough, every part of the palette has a data-cmdk-* attribute and every state is a data attribute too. This terminal look restyles the selected row, prefixes each row and heading, and strikes through disabled rows.
<script setup lang="ts">
import { ref } from 'vue'
import { CommandPalette, type Command, type CommandContext } from 'vue-cmdk-palette'
const last = ref('nothing yet')
const run = (context: CommandContext): void => {
last.value = context.command.label
}
const commands: Command[] = [
{ kind: 'action', id: 'status', label: 'git status', group: 'git', perform: run },
{ kind: 'action', id: 'pull', label: 'git pull --rebase', group: 'git', perform: run },
{ kind: 'action', id: 'build', label: 'bun run build', group: 'scripts', shortcut: 'mod+b', perform: run },
{ kind: 'action', id: 'test', label: 'bun run test', group: 'scripts', perform: run },
{ kind: 'action', id: 'lint', label: 'bun run lint', group: 'scripts', disabled: 'Already running', perform: run },
]
</script>
<template>
<div class="terminal">
<CommandPalette
:commands="commands"
:recents="false"
:autofocus="false"
:quick-select="false"
:labels="{ placeholder: 'run…' }"
/>
</div>
<p class="note">Last command: {{ last }}</p>
</template>
<style scoped>
/*
* Past the variables, every part has a data-cmdk-* attribute and the state lives in
* data-selected, data-disabled and data-active, so ordinary CSS can restyle anything.
*/
.terminal {
--cmdk-font: var(--cmdk-font-mono);
--cmdk-radius: 4px;
--cmdk-row-radius: 0;
--cmdk-bg: #0b0f0c;
--cmdk-bg-sunken: #0b0f0c;
--cmdk-fg: #b8f5c4;
--cmdk-fg-muted: #6fbf80;
--cmdk-fg-faint: #4b8a5a;
--cmdk-border: #1f3b27;
--cmdk-accent: #39d353;
height: 300px;
display: flex;
flex-direction: column;
}
.terminal :deep([data-cmdk-root]) {
flex: 1;
min-height: 0;
}
.terminal :deep([data-cmdk-group-heading])::before {
content: '# ';
}
.terminal :deep([data-cmdk-item]) {
min-height: 30px;
}
.terminal :deep([data-cmdk-item][data-selected='true']) {
color: #0b0f0c;
background: var(--cmdk-accent);
box-shadow: none;
}
.terminal :deep([data-cmdk-item][data-selected='true'] *) {
color: inherit;
}
.terminal :deep([data-cmdk-item])::before {
content: '$';
opacity: 0.5;
}
.terminal :deep([data-cmdk-item][data-disabled='true']) {
text-decoration: line-through;
}
</style>
Headless
useCommandPalette() with your own input and list. The composable gives the ranking, the selection and the ARIA ids, and the markup is yours. Use it when your design system already has the pieces.
<script setup lang="ts">
import { ref } from 'vue'
import { useCommandPalette, type Command } from 'vue-cmdk-palette'
const last = ref('nothing yet')
const run = (label: string) => () => {
last.value = label
}
const commands: Command[] = [
{ kind: 'action', id: 'new-file', label: 'New file', perform: run('New file') },
{ kind: 'action', id: 'save', label: 'Save', shortcut: 'mod+s', perform: run('Save') },
{ kind: 'action', id: 'close', label: 'Close tab', perform: run('Close tab') },
]
// The composable owns the query, the ranking and the ARIA ids. This markup is yours.
const palette = useCommandPalette({ commands, recents: false })
</script>
<template>
<div class="headless" @keydown="palette.onContainerKeydown">
<input
v-model="palette.query.value"
class="field"
placeholder="Type a command"
v-bind="palette.fieldAttrs.value"
@keydown="palette.onFieldKeydown"
/>
<ul class="list" v-bind="palette.listAttrs.value">
<li
v-for="row in palette.list.value.rows"
:key="row.command.id"
class="row"
v-bind="palette.rowAttrs(row)"
@pointermove="palette.hover(row)"
@click="palette.activate(row)"
>
{{ row.command.label }}
</li>
</ul>
<p class="note">Last command: {{ last }}</p>
</div>
</template>
<style scoped>
.headless {
display: grid;
gap: 0.5rem;
max-width: 24rem;
}
.field {
padding: 0.5rem 0.75rem;
font: inherit;
}
.list {
margin: 0;
padding: 0;
list-style: none;
}
.row {
padding: 0.4rem 0.75rem;
cursor: pointer;
}
.row[aria-selected='true'] {
background: color-mix(in srgb, currentColor 12%, transparent);
}
</style>