Skip to content
Component installs need the registry setup
VegaStack Design

Command Search

The search palette, results first — one-row scope chips with More, results to 520px, stale results dimmed while a search runs.

Status
stable
Since
0.25.89
Accessibility pattern
dialog, combobox, listbox; aria-busy while searching

Last updated

Search

Search or jump to…

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Command Search before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify command-search

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/command-search

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/command, @vegastack/dropdown-menu, @vegastack/notification-bell, @vegastack/toggle, @vegastack/toggle-group.

Usage

"use client";

import {
  CommandSearch,
  CommandSearchInput,
  CommandSearchResults,
  CommandSearchScopes,
  CommandSearchSections,
} from "@/components/ui/command-search";
import {
  CommandFooter,
  CommandGroup,
  CommandItem,
} from "@/components/ui/command";
import { useAsyncSearch } from "@/components/ui/use-async-search";

export function SearchPalette({ open, onOpenChange }: Props) {
  const [scope, setScope] = React.useState("all");
  const [archived, setArchived] = React.useState(false);
  // Debounced and abortable; an answer that arrives after a newer request started is dropped.
  const search = useAsyncSearch(
    (q, { signal }) => api.search({ q, scope, archived }, { signal }),
    { enabled: open, deps: [scope, archived] },
  );
  return (
    <CommandSearch
      open={open}
      onOpenChange={onOpenChange}
      pending={search.loading}
    >
      <CommandSearchInput
        value={search.query}
        onValueChange={search.onSearchChange}
      />
      <CommandSearchScopes
        scopes={SCOPES}
        value={scope}
        onValueChange={setScope}
        includeArchived={archived}
        onIncludeArchivedChange={setArchived}
      />
      <CommandSearchResults>
        {search.query ? (
          <Results items={search.items} />
        ) : (
          <CommandSearchSections
            recent={recent}
            create={create}
            places={places}
          />
        )}
      </CommandSearchResults>
      <CommandFooter />
    </CommandSearch>
  );
}

A search palette is for finding things, so the results get the room: the scope chips sit on one line that never wraps — whatever does not fit goes into More, with "Include archived" — and the results run to min(60dvh, 520px). While a new answer is on its way the old one stays, dimmed and marked busy, with a thin bar running under the input, so stale results never read as the answer to what was just typed. Pair it with useAsyncSearch, which drops out-of-order answers, and pass its loading as pending.

Scope

  • Owns: the palette frame (a CommandDialog holding a Command that does not filter), the one-row scope chips and their More overflow, the archived toggle, the results' height cap, the in-flight dimming and progress bar, the start sections' order, and the preview pane.
  • Does not own: searching — the host's loader and useAsyncSearch — the results' rows, opening a result, or recents.
  • Compose with: CommandGroup, CommandItem and CommandEmpty for the results, and CommandFooter for key hints.

Anatomy

CommandSearch — data-slot="command-search"
CommandSearchInput — data-slot="command-search-input" | "command-search-progress"
CommandSearchScopes — data-slot="command-search-archived-dot" | "command-search-more" | "command-search-more-content" | "command-search-scopes" | "command-search-scopes-ruler"
CommandSearchResults — data-slot="command-search-body" | "command-search-preview"
CommandSearchSections — data-slot="command-search-section"

Examples

Results first

Open the palette: with nothing typed it lists Recent, Create and Places, in that order. Type to search — each answer takes 700ms here, so the bar under the input and the dimmed stale results are visible. Pick a scope from the chips or from More; a scope picked from More names its trigger.

Search

Search or jump to…

Preview pane

Pass preview with the highlighted result's summary and, from 1280px of viewport, the palette widens and shows it beside the results. Below 1280px the pane is not shown.

Search

Search or jump to…

API Reference

CommandSearch

PropTypeDefaultDescription
descriptionstring"Search or jump to…"The dialog's accessible description (visually hidden).
dialogClassNamestring—Classes for the dialog surface.
onOpenChange((open: boolean) => void)—Fired when the palette asks to open or close.
openboolean—Controlled open state.
pendingbooleanfalseA search is in flight: the results still shown are stale, so they dim and are marked busy, and a thin bar runs under the input.
previewReact.ReactNode—A preview of the highlighted result, in a pane beside the results from 1280px of viewport (the palette widens to hold it). Hidden below that.
resultsLabel((count: number) => string)—
shouldFilterbooleanfalsecmdk's own filtering. Off by default: a search palette shows the host's answer as given.
titlestring"Search"The dialog's accessible title (visually hidden).

Data attributes and CSS variables on CommandSearch

AttributeValues
data-pending""
data-slot"command-search"

It also takes Command's props (value, onValueChange, onKeyDown, resultsLabel).

CommandSearchInput

CommandInput's props; the placeholder defaults to "Search or jump to…".

CommandSearchScopes

PropTypeDefaultDescription
onValueChange*(value: string) => void—Fired with the scope picked from the chips or from More.
scopes*readonly CommandSearchScope[]—The scopes, in chip order; the first is usually "All".
value*string—The selected scope's value.
aria-labelstring"Search in"The chip group's accessible name.
classNamestring—Classes for the row.
includeArchivedbooleanfalseWhether archived items are included. Shown in More only with onIncludeArchivedChange.
includeArchivedLabelstring"Include archived"The archived item's text.
maxVisiblenumber—At most this many chips before More, whatever the room. Omitted, as many as fit.
moreLabelstring"More"The overflow menu's trigger text (it shows the picked scope's name instead while that scope sits in the menu).
onIncludeArchivedChange((includeArchived: boolean) => void)—Fired by More's "Include archived" item; omit it and the item is not offered.

Data attributes and CSS variables on CommandSearchScopes

AttributeValues
data-icon"inline-end"
data-measure"chip" | "more"
data-slot"command-search-archived-dot" | "command-search-more" | "command-search-more-content" | "command-search-scopes" | "command-search-scopes-ruler"
data-state"on"

CommandSearchResults

CommandList's props.

CommandSearchSections

PropTypeDefaultDescription
createReact.ReactNode—Create actions — New page, New task (CommandItems).
labels{ recent?: string; create?: string; places?: string; }{ recent: "Recent", create: "Create", places: "Places" }Section headings.
placesReact.ReactNode—Places to jump to — Home, Tasks, Library (CommandItems).
recentReact.ReactNode—Recently opened records and searches (CommandItems). Empty or omitted: no section.

Data attributes and CSS variables on CommandSearchSections

AttributeValues
data-sectionmirrors a prop or state value
data-slot"command-search-section"

Accessibility

  • The palette is a labelled dialog around cmdk's combobox and listbox; the result count is announced by Command (resultsLabel).
  • While pending, the results are aria-busy; the progress bar is decorative.
  • The visible chips are a toggle group named "Search in"; More is a menu button whose name is the scope it holds, and says "(Include archived)" while archived items are included.
  • Enter, ↑/↓ and Home/End on a chip or in the More menu stay with that control instead of moving the results' selection.
KeyAction
↑ / ↓Moves through the results (from the input)
EnterOpens the selected result
TabMoves to the scope chips and More
EscapeCloses the palette
ContractStates tested
Behaviourdefault, scope-chip, scope-overflow, include-archived, pending, stale-results, sections, preview
Accessibilitydialog-semantics, listbox, busy, menu-semantics, browser-accessibility-test
Visualdefault, pending, more-active, archived-on, with-preview

Do / Don't

Do
Keep the scopes on one line and let More hold the rest; dim the old answer while the next one loads.
Don't
Wrap the chips onto a second row above a five-result list, or leave stale results looking current.

On this page