Command
Source for this preview
------
<section id="command-demo" class="command-demo" aria-label="Command palette example"> <div class="command-demo-copy"> <span class="panel-kicker">COMMAND / QUICK ACTIONS</span> <h3>Move at the speed of thought.</h3> <p>Search grouped actions, navigate with arrow keys, and press Enter to run the active command.</p> <floating-root id="command-root"> <floating-reference><button class="command-trigger" type="button">Open command palette <kbd id="command-shortcut">Ctrl K</kbd></button></floating-reference> <dialog slot="floating" class="command-dialog" aria-labelledby="command-title"> <floating-list navigation loop allow-escape> <floating-query id="command-query" > <h4 id="command-title" class="sr-only">Command palette</h4> <div class="command-shell"> <div class="command-input-row"> <span aria-hidden="true">⌕</span> <input id="command-input" class="command-input" type="text" autocomplete="off" spellcheck="false" placeholder="Type a command or search…" aria-label="Search commands" aria-controls="command-list" autofocus /> <button class="command-close" type="button" data-fup-close aria-label="Close command palette">Esc</button> </div> <div id="command-list" class="command-list" role="listbox" aria-label="Commands"> <floating-results> <floating-results-status type="idle"> <div class="command-empty">Start typing to search commands.</div> </floating-results-status> <floating-results-status type="loading"> <div class="command-empty">Searching commands…</div> </floating-results-status> <floating-results-status type="error"> <div class="command-empty">Command search failed.</div> </floating-results-status> <floating-results-status type="empty"> <div class="command-empty">No commands found for “<span data-search-text="$query"></span>”.</div> </floating-results-status> <floating-results-item> <floating-list-item> <div class="command-item"> <span class="command-item-icon" aria-hidden="true" data-search-text="icon"></span> <span class="command-item-label"><strong data-search-text="label"></strong><small data-search-text="group"></small></span> <kbd data-search-text="shortcut"></kbd> </div> </floating-list-item> </floating-results-item> </floating-results> </div> <p id="command-status" class="sr-only" aria-live="polite">Command palette closed</p> </div> </floating-query> </floating-list> </dialog> </floating-root> <p id="command-result" class="command-result" aria-live="polite">Choose a command to continue.</p> </div></section>
<script> import {FloatingRootElement, click, createFuzzySearchSource, dismiss, type FloatingQueryElement} from '@floating-ui-plus/web-components'; import {commandItems, commandSearchKeys, type CommandItem} from '../../command-items'; import {initializeExample} from './initialize-example';
initializeExample('command', (scope) => { const root = FloatingRootElement.query(scope, '#command-root'); const result = scope.querySelector<HTMLElement>('#command-result'); const query = scope.querySelector<FloatingQueryElement>('#command-query'); if (!result || !query) return;
const isApplePlatform = /Macintosh|Mac OS X|iPhone|iPad|iPod/i.test( navigator.userAgent, ); scope.querySelector<HTMLElement>('#command-shortcut')!.textContent = isApplePlatform ? '⌘ K' : 'Ctrl K';
root.configure({plugins: [click(), dismiss()]}); query.configure<CommandItem>({ search: { source: createFuzzySearchSource(commandItems, { keys: commandSearchKeys, threshold: 0.35, }), getItemKey: (item) => item.id, debounceMs: 0, }, getItemLabel: (item) => item.label, status: { closed: 'Command palette closed', idle: 'Start typing to search commands', loading: 'Searching commands', error: 'Command search failed', empty: ({search}) => `No commands found for ${search.query}`, results: ({search}) => `${search.items.length} commands available`, }, });
const activate = (event: Event) => { const {item} = (event as CustomEvent<{item: CommandItem}>).detail; result.textContent = `${item.label} selected.`; }; const shortcut = (event: KeyboardEvent) => { const usesPrimaryModifier = isApplePlatform ? event.metaKey : event.ctrlKey; if (usesPrimaryModifier && event.key.toLowerCase() === 'k') { event.preventDefault(); if (root.open) { root.floatingElement?.querySelector<HTMLInputElement>('#command-input')?.focus(); } else { root.open = true; } } }; query.addEventListener('queryactivate', activate); document.addEventListener('keydown', shortcut); return () => { query.removeEventListener('queryactivate', activate); document.removeEventListener('keydown', shortcut); }; });</script><script setup lang="ts">import { FloatingContent, FloatingList, FloatingListItem, FloatingReference, FloatingResults, FloatingRoot, click, createFuzzySearchSource, dismiss, useQuery, useSearch,} from "@floating-ui-plus/vue";import { computed, onBeforeUnmount, onMounted, ref } from "vue";import { commandItems, commandSearchKeys, type CommandItem,} from "../../command-items";
const input = ref<HTMLInputElement>();const selected = ref("Choose a command to continue.");const source = createFuzzySearchSource(commandItems, { keys: commandSearchKeys, threshold: 0.35,});const search = useSearch<CommandItem>({ source, getItemKey: (item) => item.id, debounceMs: 0,});const command = useQuery({ search, semantics: "dialog", getItemLabel: (item) => item.label, status: { closed: "Command palette closed", idle: "Start typing to search commands", loading: "Searching commands", error: "Command search failed", empty: ({ search: state }) => `No commands found for ${state.query}`, results: ({ search: state }) => `${state.items.length} commands available`, }, onActivate(item) { selected.value = `${item.label} selected.`; open.value = false; },});const { open, activeIndex, inputProps, getItemId, getNavigationOptions, getOptionProps, statusText,} = command;const plugins = [click(), dismiss(), command.rolePlugin];const navigationOptions = getNavigationOptions({ allowEscape: true, loop: true,});const activeId = computed(() => activeIndex.value == null ? undefined : getItemId(activeIndex.value),);const shortcutLabel = typeof navigator !== "undefined" && /Macintosh|Mac OS X|iPhone|iPad|iPod/i.test(navigator.userAgent) ? "⌘ K" : "Ctrl K";
function handleShortcut(event: KeyboardEvent) { const usesPrimaryModifier = shortcutLabel === "⌘ K" ? event.metaKey : event.ctrlKey; if (usesPrimaryModifier && event.key.toLowerCase() === "k") { event.preventDefault(); if (open.value) { input.value?.focus(); } else { open.value = true; } }}
onMounted(() => document.addEventListener("keydown", handleShortcut));onBeforeUnmount(() => document.removeEventListener("keydown", handleShortcut));</script>
<template> <section class="command-demo vue-command-demo" aria-label="Command palette example" > <div class="command-demo-copy"> <span class="panel-kicker">COMMAND / QUICK ACTIONS</span> <h3>Move at the speed of thought.</h3> <p> Search grouped actions, navigate with arrow keys, and press Enter to run the active command. </p> <FloatingRoot v-model:open="open" :plugins="plugins"> <FloatingReference class="command-trigger"> Open command palette <kbd>{{ shortcutLabel }}</kbd> </FloatingReference> <FloatingContent as="dialog" class="command-dialog" aria-labelledby="vue-command-title" > <h4 id="vue-command-title" class="sr-only">Command palette</h4> <FloatingList v-model:active-index="activeIndex" navigation loop :navigation-options="navigationOptions" > <div class="command-shell"> <div class="command-input-row"> <span aria-hidden="true">⌕</span> <input ref="input" class="command-input" type="text" autocomplete="off" spellcheck="false" placeholder="Type a command or search…" aria-label="Search commands" aria-controls="vue-command-list" :aria-activedescendant="activeId" autofocus v-bind="inputProps" /> <button class="command-close" type="button" data-fup-close aria-label="Close command palette" > Esc </button> </div> <div id="vue-command-list" class="command-list" role="listbox" aria-label="Commands" > <FloatingResults :search="search"> <template #idle> <div class="command-empty">Start typing to search commands.</div> </template> <template #loading> <div class="command-empty">Searching commands…</div> </template> <template #error> <div class="command-empty">Command search failed.</div> </template> <template #empty> <div class="command-empty"> No commands found for “{{ search.query.value }}”. </div> </template> <template #results> <FloatingListItem v-for="(item, index) in search.items.value" :key="item.id" :label="item.label" :value="item" v-bind="getOptionProps(item, index)" > <div class="command-item"> <span class="command-item-icon" aria-hidden="true">{{ item.icon }}</span> <span class="command-item-label"> <strong>{{ item.label }}</strong> <small>{{ item.group }}</small> </span> <kbd v-if="item.shortcut">{{ item.shortcut }}</kbd> </div> </FloatingListItem> </template> </FloatingResults> </div> <p class="sr-only" aria-live="polite"> {{ statusText }} </p> </div> </FloatingList> </FloatingContent> </FloatingRoot> <p class="command-result" aria-live="polite">{{ selected }}</p> </div> </section></template>This example follows the shadcn Base Command composition: Command Input, List, Empty, Group, Separator, Item, and Shortcut are styling-oriented application parts inside a modal command dialog. Floating UI Plus owns the shared search, keyboard list navigation, dialog dismissal, and focus lifecycle.
Behavior
Section titled “Behavior”- The trigger or
Command/Ctrl + Kopens the native dialog and focuses the search input. createFuzzySearchSource()searches labels, keywords, and group names.- Arrow keys move the active item while focus remains in the input. Enter runs the active command and closes the dialog. Escape closes without selection.
- Empty results replace the scrollable grouped list.
- Group headings, separators, and shortcut labels remain application markup; they do not need a new positioning primitive.
const search = useSearch({ source: createFuzzySearchSource(commandItems, { keys: commandSearchKeys, threshold: 0.35, }), getItemKey: (item) => item.id, debounceMs: 0,});
const command = useQuery({ search, semantics: "dialog", getItemLabel: (item) => item.label, onActivate(item) { runCommand(item); },});The Web Components version keeps the native dialog as the root’s direct
<dialog slot="floating"> surface, with <floating-query>,
<floating-results>, <floating-list navigation>, and <floating-list-item>
inside it. Those elements own query input bindings, IME handling, search phase
rendering, result registration, arrow-key navigation, Enter activation, and
the dialog role contract. Vue uses the equivalent
useSearch()/useQuery() state with <FloatingList> and
<FloatingListItem>. Both adapters reuse the same search source; neither
needs a Command-specific package primitive.
This matches the modal and Sheet examples: use <dialog slot="floating"> for
a modal native surface. Reserve <template slot="content"> for ordinary
conditional Popover content, rather than wrapping a dialog in that template.
Because this query is inside a native dialog, Web Components infers dialog
semantics and generates unique option IDs; the example therefore does not need
semantics="dialog" or option-id-prefix. Keep semantics="dialog" when a
query is rendered in a non-native dialog surface.
The Command palette is viewport-centered by its dialog CSS. A direct native
<dialog> surface skips Floating UI positioning styles, so no placement or
transform override is needed. The list itself has a bounded block size and
overflow-y: auto, allowing groups to remain usable on short mobile screens.
The component names in shadcn are composition names rather than APIs added by Floating UI Plus. Keeping those wrappers in application code preserves the package boundary: search and interaction behavior are reusable while the command data model, grouping, icons, shortcuts, and executed actions remain product-specific.