Skip to content
Kappa
Command Palette
@dicehub/kappav0.4.2

Command Palette

Search and run commands from an accessible modal combobox.

<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

Typical part hierarchy
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.Footer

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

Keyboard Navigation

Built-in keyboard navigation follows the active Combobox collection:

↑↓Move the highlight between enabled commands.
HomeEndMove to the first or last enabled command.
EnterRun the highlighted command.
⌘/CtrlEnterRun with newTab set to true.
EscapeClose the dialog and restore focus.

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>

Component Parts

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.

Contains keyboard help or related status information.

Accessibility

  • Dialog traps focus, blocks background interaction, and restores focus after dismissal.
  • 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

PropTypeDefaultDescription
openboolean-Controlled dialog state; supports v-model:open.
defaultOpenbooleanfalseInitial dialog state for uncontrolled use.
valuestring-Controlled search query; supports v-model:value.
defaultValuestring""Initial uncontrolled search query.
itemsT[][]Flat results or top-level groups rendered by Results.
getSelectableItems(items: T[]) => T[]identityFlattens grouped data into keyboard-selectable items.
itemToStringValue(item: T) => stringtitle / label / name / valueMaps an object item to searchable text.
itemToValue(item: T) => stringid / value / title / labelMaps an item to its stable Ark UI value.
isItemDisabled(item: T) => booleanitem.disabledMarks commands unavailable and skips them during navigation.
filterfalse | (item, query) => booleancontains textFilters flat items. Use false for externally filtered groups.
ariaLabelstring"Command palette"Accessible name for the dialog.
modalbooleantrueControls focus trapping and outside interaction.

ResultItem

PropTypeDefaultDescription
valueT-Item passed to selection events.
titlestring-Primary result label.
breadcrumbsstring[][]Hierarchy rendered before the title.
descriptionstring-Supporting result text.
titleHighlights[number, number][][]Inclusive title match ranges.
breadcrumbHighlights[number, number][][][]Inclusive ranges for each breadcrumb.
showArrowbooleantrueShows the destination arrow.
externalbooleanfalseShows an external-destination cue.
nonInteractivebooleanfalseRenders rich result content without activation.

Events

EventPayloadDescription
update:openbooleanUpdates controlled dialog state.
openChangebooleanReports each dialog state change.
update:valuestringUpdates the controlled search query.
valueChangestringReports each search query change.
selectitem, { newTab, event? }Reports activation and modified-key intent.
itemHighlighteditem, { index, reason }Reports keyboard, pointer, and reset highlights.
closevoidReports closure after dismissal or a state change.