<script setup>
import { computed, ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const query = ref("");
const selected = ref("");
const groups = [
{
label: "Commands",
items: [
{ id: "create", title: "Create new project" },
{ id: "settings", title: "Open settings" },
{ id: "search", title: "Search files" },
],
},
{
label: "Pages",
items: [
{ id: "home", title: "Home" },
{ id: "dashboard", title: "Dashboard" },
],
},
];
const filteredGroups = computed(() => groups
.map((group) => ({
...group,
items: group.items.filter((item) =>
item.title.toLowerCase().includes(query.value.toLowerCase()),
),
}))
.filter((group) => group.items.length));
const flatten = (items) => items.flatMap((group) => group.items);
const title = (item) => item.title;
const select = (item) => {
selected.value = item.title;
query.value = "";
open.value = false;
};
</script>
<template>
<Button @click="open = true">Open Command Palette</Button>
<p v-if="selected">Last selected: {{ selected }}</p>
<CommandPalette.Root
v-model:open="open"
v-model:value="query"
:filter="false"
:get-selectable-items="flatten"
:item-to-string-value="title"
:items="filteredGroups"
@select="select"
>
<CommandPalette.Input placeholder="Type a command or search..." />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item: group }">
<CommandPalette.Group :items="group.items">
<CommandPalette.GroupLabel>{{ group.label }}</CommandPalette.GroupLabel>
<CommandPalette.Items v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item.title }}</CommandPalette.Item>
</CommandPalette.Items>
</CommandPalette.Group>
</CommandPalette.Results>
<CommandPalette.Empty>No commands found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer>
<span><kbd>↑↓</kbd> Navigate</span>
<span><kbd>Enter</kbd> Select</span>
</CommandPalette.Footer>
</CommandPalette.Root>
</template>Installation
Barrel
import { CommandPalette } from "@dicehub/kappa";Granular
import { CommandPalette } from "@dicehub/kappa/components/command-palette";Usage
CommandPalette is a compound component built on Ark UI Dialog and Combobox primitives. Pass a flat item array for built-in filtering, or control the query and flatten visible groups with getSelectableItems.
<script setup>
import { ref } from "vue";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const commands = [
{ id: "create", title: "Create project" },
{ id: "settings", title: "Open settings" },
];
</script>
<template>
<button @click="open = true">Open</button>
<CommandPalette.Root v-model:open="open" :items="commands" @select="open = false">
<CommandPalette.Input placeholder="Search..." />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item.title }}</CommandPalette.Item>
</CommandPalette.Results>
<CommandPalette.Empty>No results.</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</template>Composition
CommandPalette.Root
└── CommandPalette.Dialog
└── CommandPalette.Panel
├── CommandPalette.Input
├── CommandPalette.List
│ ├── CommandPalette.Empty
│ ├── CommandPalette.Loading
│ └── CommandPalette.Results
│ └── CommandPalette.Group
│ ├── CommandPalette.GroupLabel
│ └── CommandPalette.Items
│ └── CommandPalette.Item
└── CommandPalette.FooterRoot provides the usual modal command surface. Dialog and Panel are also public so an application can keep one modal mounted while it swaps command levels or result sources.
<CommandPalette.Root>
<CommandPalette.Dialog>
<CommandPalette.Panel>
<CommandPalette.Input />
<CommandPalette.List>
<CommandPalette.Empty />
<CommandPalette.Results>
<CommandPalette.Group>
<CommandPalette.GroupLabel />
<CommandPalette.Items>
<CommandPalette.Item />
</CommandPalette.Items>
</CommandPalette.Group>
</CommandPalette.Results>
</CommandPalette.List>
<CommandPalette.Footer />
</CommandPalette.Panel>
</CommandPalette.Dialog>
</CommandPalette.Root>Flat lists
Render Results directly. Object items use itemToStringValue when their searchable label is not one of the default keys.
Groups and collections
Render Group, GroupLabel, and Items for each visible group. Flatten only visible group items for keyboard navigation.
Single activation
Unlike a multi-select Combobox, a command palette runs one highlighted command. Chips, multiple selection, and a clear-selection control do not apply.
Examples
With Grouped Items
Group related commands with labels. The first enabled command is highlighted automatically, and disabled commands remain visible but are skipped.
<script setup>
import { computed, ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const query = ref("");
const selected = ref("");
const groups = [
{
label: "Commands",
items: [
{ id: "create", title: "Create new project" },
{ id: "settings", title: "Open settings" },
{ id: "search", title: "Search files" },
],
},
{
label: "Pages",
items: [
{ id: "home", title: "Home" },
{ id: "dashboard", title: "Dashboard" },
],
},
];
const filteredGroups = computed(() => groups
.map((group) => ({
...group,
items: group.items.filter((item) =>
item.title.toLowerCase().includes(query.value.toLowerCase()),
),
}))
.filter((group) => group.items.length));
const flatten = (items) => items.flatMap((group) => group.items);
const title = (item) => item.title;
const select = (item) => {
selected.value = item.title;
query.value = "";
open.value = false;
};
</script>
<template>
<Button @click="open = true">Open Command Palette</Button>
<p v-if="selected">Last selected: {{ selected }}</p>
<CommandPalette.Root
v-model:open="open"
v-model:value="query"
:filter="false"
:get-selectable-items="flatten"
:item-to-string-value="title"
:items="filteredGroups"
@select="select"
>
<CommandPalette.Input placeholder="Type a command or search..." />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item: group }">
<CommandPalette.Group :items="group.items">
<CommandPalette.GroupLabel>{{ group.label }}</CommandPalette.GroupLabel>
<CommandPalette.Items v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item.title }}</CommandPalette.Item>
</CommandPalette.Items>
</CommandPalette.Group>
</CommandPalette.Results>
<CommandPalette.Empty>No commands found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer>
<span><kbd>↑↓</kbd> Navigate</span>
<span><kbd>Enter</kbd> Select</span>
</CommandPalette.Footer>
</CommandPalette.Root>
</template>Simple Flat List
Use a flat array for a compact command list. Built-in filtering uses title, label, name, or value text.
<script setup>
import { ref } from "vue";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const actions = [
{ id: "copy", title: "Copy" },
{ id: "paste", title: "Paste" },
{ id: "cut", title: "Cut" },
{ id: "delete", title: "Delete" },
];
</script>
<template>
<button @click="open = true">Open Simple Palette</button>
<CommandPalette.Root v-model:open="open" :items="actions" @select="open = false">
<CommandPalette.Input placeholder="Search actions..." />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item.title }}</CommandPalette.Item>
</CommandPalette.Results>
<CommandPalette.Empty>No actions found.</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</template>Loading State
Replace results with an accessible loading status while commands load from a local or remote index.
<script setup>
import { ref } from "vue";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const loading = ref(false);
const items = ref([]);
const openPalette = async () => {
open.value = true;
loading.value = true;
items.value = await loadCommands();
loading.value = false;
};
</script>
<template>
<button @click="openPalette">Open with Loading</button>
<CommandPalette.Root v-model:open="open" :items="loading ? [] : items">
<CommandPalette.Input placeholder="Search..." />
<CommandPalette.List>
<CommandPalette.Loading v-if="loading" />
<template v-else>
<CommandPalette.Results v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item.title }}</CommandPalette.Item>
</CommandPalette.Results>
<CommandPalette.Empty>No results found.</CommandPalette.Empty>
</template>
</CommandPalette.List>
</CommandPalette.Root>
</template>Disabling Browser Autocomplete
Forward standard input and password-manager attributes when browser overlays would compete with palette results.
<CommandPalette.Input
autocomplete="off"
autocapitalize="none"
autocorrect="off"
data-1p-ignore="true"
data-lpignore="true"
placeholder="Search commands..."
:spellcheck="false"
/>ResultItem with Breadcrumbs
Use ResultItem for object items that need breadcrumbs, descriptions, icons, match highlights, or destination cues.
<script setup>
import { ref } from "vue";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const open = ref(false);
const results = [
{ id: "button", title: "Button", breadcrumbs: ["Components"] },
{ id: "dialog", title: "Dialog", breadcrumbs: ["Components"] },
{ id: "header", title: "Page Header", breadcrumbs: ["Blocks"] },
];
</script>
<template>
<button @click="open = true">Open with ResultItem</button>
<CommandPalette.Root v-model:open="open" :items="results" @select="open = false">
<CommandPalette.Input placeholder="Search documentation..." />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }">
<CommandPalette.ResultItem
:breadcrumbs="item.breadcrumbs"
:description="item.description"
:title="item.title"
:value="item"
/>
</CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</template>Top-aligned search
Click the search field, or press / while it has focus. The panel expands and fades in at the top of the screen. Reduced motion removes the animation.
<script setup lang="ts">
import { ref } from "vue";
import { Search } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const container = ref<HTMLElement>();
const open = ref(false);
const query = ref("");
const selected = ref("");
const pages = ["Dashboard", "Projects", "Recently opened", "Activities", "Settings"];
function showSearch() {
container.value?.querySelector<HTMLElement>("[data-search-trigger]")?.focus();
query.value = "";
open.value = true;
}
function shortcut(event: KeyboardEvent) {
if (event.key !== "/" || event.defaultPrevented || event.repeat || event.isComposing || event.metaKey || event.ctrlKey || event.altKey || open.value) return;
const target = event.composedPath().find(node => node instanceof HTMLElement) as HTMLElement | undefined;
if (target?.isContentEditable || target?.closest('input, textarea, select, [role="textbox"], [role="combobox"], [role="searchbox"], [role="dialog"], [role="menu"]')) return;
event.preventDefault();
event.stopPropagation();
showSearch();
}
</script>
<template>
<div ref="container" class="command-palette-top-search-demo" @keydown="shortcut">
<Button variant="ghost" :icon="Search" class="command-palette-top-search-demo__trigger" data-search-trigger aria-label="Search or go to…" aria-keyshortcuts="/" aria-haspopup="dialog" :aria-expanded="open" @click="showSearch">
<span>Search or go to…</span><kbd aria-hidden="true">/</kbd>
</Button>
<p v-if="selected" role="status">Opened {{ selected }}.</p>
<CommandPalette.Dialog v-model:open="open" class="command-palette-top-search-dialog" aria-label="Top-aligned search">
<CommandPalette.Panel v-model:value="query" :open="open" :items="pages" @close="open = false" @select="item => { selected = String(item); open = false; }">
<CommandPalette.Input placeholder="Search or go to…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No results found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span><span>Esc Close</span></CommandPalette.Footer>
</CommandPalette.Panel>
</CommandPalette.Dialog>
</div>
</template>
<style>
.command-palette-top-search-demo { inline-size: 21.5rem; max-inline-size: 100%; margin-inline: auto; }
.command-palette-top-search-demo__trigger.kappa-button { inline-size: 100%; min-inline-size: 0; block-size: 2rem; min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.75rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-subtle); }
.command-palette-top-search-demo__trigger.kappa-button[data-variant="ghost"] { --kappa-button-hover-background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); --kappa-button-hover-foreground: var(--kappa-subtle); }
.command-palette-top-search-demo__trigger.kappa-button:hover { border-color: color-mix(in srgb, var(--kappa-line) 90%, var(--kappa-subtle)); }
.command-palette-top-search-demo__trigger .kappa-button__label { display: flex; flex: 1; align-items: center; gap: 0.5rem; min-inline-size: 0; }
.command-palette-top-search-demo__trigger .kappa-button__label > span { flex: 1; overflow: hidden; text-align: start; text-overflow: ellipsis; white-space: nowrap; }
.command-palette-top-search-demo__trigger kbd { display: grid; place-items: center; min-inline-size: 1.25rem; block-size: 1.25rem; border: 1px solid var(--kappa-line); border-radius: 0.1875rem; font: 0.75rem / 1 var(--kappa-font-mono); }
.kappa-command-palette__positioner:has(> .command-palette-top-search-dialog) { padding: 0.25rem 0.5rem 0.5rem; }
.command-palette-top-search-dialog.kappa-command-palette__content { inline-size: min(48rem, calc(100vw - 1rem)); max-block-size: calc(100dvh - 0.75rem); border: 0; border-radius: 1rem; transform-origin: center top; }
.command-palette-top-search-dialog .kappa-command-palette__input-header { min-block-size: 2.5rem; margin: 0.25rem; padding-inline: 0.5rem; gap: 0.5rem; border: 0; border-radius: 0.75rem; background: var(--kappa-control); }
.command-palette-top-search-dialog .kappa-command-palette__input-header:focus-within { box-shadow: inset 0 0 0 2px var(--kappa-focus); }
.command-palette-top-search-dialog .kappa-command-palette__input { font-size: 0.875rem; }
.command-palette-top-search-dialog .kappa-command-palette__list { border-block-start: 1px solid var(--kappa-line); }
.command-palette-top-search-dialog.kappa-command-palette__content[data-state="open"] { animation: command-palette-top-search-in 200ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.command-palette-top-search-dialog.kappa-command-palette__content[data-state="closed"] { animation: command-palette-top-search-out 200ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.kappa-command-palette__backdrop:has(+ .kappa-command-palette__positioner > .command-palette-top-search-dialog) { background: color-mix(in srgb, var(--kappa-default) 24%, transparent); backdrop-filter: none; animation-duration: 200ms; }
@keyframes command-palette-top-search-in { from { opacity: 0; scale: 0.8; } to { opacity: 1; scale: 1; } }
@keyframes command-palette-top-search-out { from { opacity: 1; scale: 1; } to { opacity: 0; scale: 0.8; } }
@media (max-width: 767px) {
.command-palette-top-search-dialog .kappa-command-palette__input { font-size: 1rem; }
}
</style>Morphing search
Click the search field, or press / while it has focus. The field expands into a search panel and returns when closed. Reduced motion opens it without animation.
<script setup lang="ts">
import { ref } from "vue";
import { Search } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
const container = ref<HTMLElement>();
const body = ref<HTMLElement>();
const open = ref(false);
const query = ref("");
const selected = ref("");
const motion = ref<Record<string, string>>({});
const pages = ["Dashboard", "Projects", "Recently opened", "Activities", "Settings"];
function rectangleProperties(prefix: string, rectangle: DOMRect) {
return {
[`--kappa-morph-${prefix}-width`]: `${rectangle.width}px`,
[`--kappa-morph-${prefix}-height`]: `${rectangle.height}px`,
[`--kappa-morph-${prefix}-x`]: `${rectangle.x + rectangle.width / 2 - window.innerWidth / 2}px`,
[`--kappa-morph-${prefix}-y`]: `${rectangle.y - 4}px`,
};
}
function setOpen(next: boolean) {
if (next === open.value) return;
const trigger = container.value?.querySelector<HTMLElement>("[data-morph-search-trigger]");
if (!trigger) return;
const panel = body.value?.closest<HTMLElement>(".command-palette-morph-dialog");
const origin = trigger.getBoundingClientRect();
const current = panel?.getBoundingClientRect() ?? origin;
const appearance = panel ? getComputedStyle(panel) : undefined;
motion.value = {
...rectangleProperties("start", current),
...rectangleProperties("origin", origin),
"--kappa-morph-start-radius": appearance?.borderRadius ?? "0.75rem",
"--kappa-morph-start-opacity": appearance?.opacity ?? "1",
};
if (next) {
trigger.focus();
query.value = "";
}
open.value = next;
}
function select(item: unknown) {
selected.value = String(item);
setOpen(false);
}
function shortcut(event: KeyboardEvent) {
if (event.key !== "/" || event.defaultPrevented || event.repeat || event.isComposing || event.metaKey || event.ctrlKey || event.altKey || open.value) return;
if (document.querySelector('[role="dialog"][data-state="open"], [role="menu"][data-state="open"]')) return;
const target = event.composedPath().find(node => node instanceof HTMLElement) as HTMLElement | undefined;
if (target?.isContentEditable || target?.closest('input, textarea, select, [role="textbox"], [role="combobox"], [role="searchbox"], [role="dialog"], [role="menu"], [role="listbox"]')) return;
event.preventDefault();
event.stopPropagation();
setOpen(true);
}
</script>
<template>
<div ref="container" class="command-palette-morph-demo" @keydown="shortcut">
<Button variant="ghost" :icon="Search" class="command-palette-morph-demo__trigger" data-morph-search-trigger aria-label="Search or go to…" aria-keyshortcuts="/" aria-haspopup="dialog" :aria-expanded="open" @click="setOpen(true)">
<span>Search or go to…</span><kbd aria-hidden="true">/</kbd>
</Button>
<p class="command-palette-morph-demo__status" role="status">{{ selected ? `Opened ${selected}.` : "" }}</p>
<CommandPalette.Dialog :open="open" :style="motion" class="command-palette-morph-dialog" aria-label="Morphing search" @update:open="setOpen">
<div ref="body" class="command-palette-morph-dialog__body">
<CommandPalette.Panel v-model:value="query" :open="open" :items="pages" @close="setOpen(false)" @select="select">
<CommandPalette.Input placeholder="Type to search…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No results found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span><span>Esc Close</span></CommandPalette.Footer>
</CommandPalette.Panel>
</div>
</CommandPalette.Dialog>
</div>
</template>
<style>
.command-palette-morph-demo { inline-size: 21.5rem; max-inline-size: 100%; margin-inline: auto; }
.command-palette-morph-demo__trigger.kappa-button { inline-size: 100%; min-inline-size: 0; block-size: 2rem; min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.75rem; background: color-mix(in srgb, var(--kappa-control) 50%, var(--kappa-tint)); color: var(--kappa-subtle); }
.command-palette-morph-demo__trigger.kappa-button[data-variant="ghost"] { --kappa-button-hover-background: color-mix(in srgb, var(--kappa-control) 50%, var(--kappa-tint)); --kappa-button-hover-foreground: var(--kappa-subtle); }
.command-palette-morph-demo__trigger.kappa-button:hover { border-color: color-mix(in srgb, var(--kappa-line) 90%, var(--kappa-subtle)); }
.command-palette-morph-demo__trigger .kappa-button__label { display: flex; flex: 1; align-items: center; gap: 0.5rem; min-inline-size: 0; }
.command-palette-morph-demo__trigger .kappa-button__label > span { flex: 1; overflow: hidden; text-align: start; text-overflow: ellipsis; white-space: nowrap; }
.command-palette-morph-demo__trigger kbd { display: grid; place-items: center; min-inline-size: 1.25rem; block-size: 1.25rem; border: 1px solid var(--kappa-line); border-radius: 0.1875rem; font: 0.75rem / 1 var(--kappa-font-mono); }
.command-palette-morph-demo__status { block-size: 1.5rem; margin: 0.5rem 0 0; overflow: hidden; color: var(--kappa-subtle); font-size: 0.875rem; line-height: 1.5rem; white-space: nowrap; text-overflow: ellipsis; }
.kappa-command-palette__positioner:has(> .command-palette-morph-dialog) { padding: 0.25rem 0.5rem 0.5rem; }
.command-palette-morph-dialog.kappa-command-palette__content { inline-size: min(48rem, calc(100vw - 1rem)); block-size: min(20rem, calc(100dvh - 0.75rem)); max-block-size: calc(100dvh - 0.75rem); border: 0; border-radius: 1rem; }
.command-palette-morph-dialog__body { display: flex; flex: 1; flex-direction: column; min-inline-size: 0; min-block-size: 0; }
.command-palette-morph-dialog .kappa-command-palette__input-header { min-block-size: 2.5rem; margin: 0.25rem; padding-inline: 0.5rem; gap: 0.5rem; border: 0; border-radius: 0.75rem; background: var(--kappa-control); }
.command-palette-morph-dialog .kappa-command-palette__input-header:focus-within { box-shadow: inset 0 0 0 2px var(--kappa-focus); }
.command-palette-morph-dialog .kappa-command-palette__input { font-size: 0.875rem; }
.command-palette-morph-dialog .kappa-command-palette__list { border-block-start: 1px solid var(--kappa-line); }
.command-palette-morph-dialog.kappa-command-palette__content[data-state="open"] { animation: command-palette-morph-in 200ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.command-palette-morph-dialog.kappa-command-palette__content[data-state="closed"] { animation: command-palette-morph-out 200ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.command-palette-morph-dialog[data-state="open"] :is(.kappa-command-palette__list, .kappa-command-palette__footer) { animation: command-palette-morph-results-in 200ms ease-out; }
.kappa-command-palette__backdrop:has(+ .kappa-command-palette__positioner > .command-palette-morph-dialog) { background: color-mix(in srgb, var(--kappa-default) 24%, transparent); backdrop-filter: none; animation-duration: 200ms; }
@keyframes command-palette-morph-in {
from { inline-size: var(--kappa-morph-start-width); block-size: var(--kappa-morph-start-height); translate: var(--kappa-morph-start-x) var(--kappa-morph-start-y); border-radius: var(--kappa-morph-start-radius); opacity: var(--kappa-morph-start-opacity); }
to { inline-size: min(48rem, calc(100vw - 1rem)); block-size: min(20rem, calc(100dvh - 0.75rem)); translate: 0 0; border-radius: 1rem; opacity: 1; }
}
@keyframes command-palette-morph-out {
from { inline-size: var(--kappa-morph-start-width); block-size: var(--kappa-morph-start-height); translate: var(--kappa-morph-start-x) var(--kappa-morph-start-y); border-radius: var(--kappa-morph-start-radius); opacity: var(--kappa-morph-start-opacity); }
to { inline-size: var(--kappa-morph-origin-width); block-size: var(--kappa-morph-origin-height); translate: var(--kappa-morph-origin-x) var(--kappa-morph-origin-y); border-radius: 0.75rem; opacity: 0; }
}
@keyframes command-palette-morph-results-in { from, 35% { opacity: 0; } to { opacity: 1; } }
@media (max-width: 767px) {
.command-palette-morph-dialog .kappa-command-palette__input { font-size: 1rem; }
}
@media (prefers-reduced-motion: reduce) {
.command-palette-morph-dialog, .command-palette-morph-dialog :is(.kappa-command-palette__list, .kappa-command-palette__footer) { animation: none !important; }
}
</style>Component Parts
Dialog accepts ids for its parts. Use :ids="{ content: 'workspace-search' }" and set the opening button's aria-controls="workspace-search".
CommandPalette.Root
Combines Dialog and Panel and owns controlled or uncontrolled state.
CommandPalette.Dialog
Provides the modal layer, focus trap, dismissal, portal, and focus restoration.
CommandPalette.Panel
Provides the Combobox collection, filtering, highlighting, and selection state.
CommandPalette.Input
Forwards input attributes and handles search and keyboard activation.
CommandPalette.List
Contains the scrollable result area.
CommandPalette.Results
Iterates the visible top-level items or groups.
CommandPalette.Group
Groups related commands and supplies items to Items.
CommandPalette.GroupLabel
Labels a command group.
CommandPalette.Items
Iterates commands inside the nearest Group.
CommandPalette.Item
Provides one selectable or disabled command.
CommandPalette.ResultItem
Adds breadcrumbs, description, icon, highlights, and destination cues.
CommandPalette.HighlightedText
Marks inclusive match ranges without using HTML injection.
CommandPalette.Empty
Displays the no-results state.
CommandPalette.Loading
Displays an accessible loading status.
CommandPalette.Footer
Contains keyboard help or related status information.
Accessibility
- Dialog traps focus, blocks background interaction, and restores focus after dismissal.
- Closing panels cannot receive focus during an exit animation.
- The search input receives initial focus and has a configurable accessible name.
- Disabled commands remain perceivable and are skipped during keyboard navigation.
- Empty and Loading expose complete no-result and progress states.
- Overlay and result motion honor
prefers-reduced-motion.
API Reference
Root
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled dialog state; supports v-model:open. |
defaultOpen | boolean | false | Initial dialog state for uncontrolled use. |
value | string | - | Controlled search query; supports v-model:value. |
defaultValue | string | "" | Initial uncontrolled search query. |
items | T[] | [] | Flat results or top-level groups rendered by Results. |
getSelectableItems | (items: T[]) => T[] | identity | Flattens grouped data into keyboard-selectable items. |
itemToStringValue | (item: T) => string | title / label / name / value | Maps an object item to searchable text. |
itemToValue | (item: T) => string | id / value / title / label | Maps an item to its stable Ark UI value. |
isItemDisabled | (item: T) => boolean | item.disabled | Marks commands unavailable and skips them during navigation. |
filter | false | (item, query) => boolean | contains text | Filters flat items. Use false for externally filtered groups. |
ariaLabel | string | "Command palette" | Accessible name for the dialog. |
modal | boolean | true | Controls focus trapping and outside interaction. |
ResultItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | - | Item passed to selection events. |
title | string | - | Primary result label. |
breadcrumbs | string[] | [] | Hierarchy rendered before the title. |
description | string | - | Supporting result text. |
titleHighlights | [number, number][] | [] | Inclusive title match ranges. |
breadcrumbHighlights | [number, number][][] | [] | Inclusive ranges for each breadcrumb. |
showArrow | boolean | true | Shows the destination arrow. |
external | boolean | false | Shows an external-destination cue. |
nonInteractive | boolean | false | Renders rich result content without activation. |
Events
| Event | Payload | Description |
|---|---|---|
update:open | boolean | Updates controlled dialog state. |
openChange | boolean | Reports each dialog state change. |
update:value | string | Updates the controlled search query. |
valueChange | string | Reports each search query change. |
select | item, { newTab, event? } | Reports activation and modified-key intent. |
itemHighlighted | item, { index, reason } | Reports keyboard, pointer, and reset highlights. |
close | void | Reports closure after dismissal or a state change. |