Skip to content

Command Palette

Terminal-styled command palette with built-in search filtering, grouped items, and keyboard navigation. Uses native dialog element with backdrop blur. Two orthogonal visual axes on Content (frame and density) keep visual chrome configurable without touching internals.

Preview

Installation

$pnpm exec dgadd add ui/command-palette
[Installs to]src/components/ui/command-palette[Item]ui/command-palette

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
"use client";import { useState } from "react";import { Button } from "@/components/ui/button";import { CommandPalette } from "@/components/ui/command-palette";export default function CommandPaletteDemo() {  const [open, setOpen] = useState(false);  return (    <div>      <Button onClick={() => setOpen(true)}>Open Command Palette</Button>      <CommandPalette open={open} onOpenChange={setOpen}>        <CommandPalette.Content>          <CommandPalette.Input placeholder="Type a command or search…" />          <CommandPalette.List>            <CommandPalette.Empty>No matching commands.</CommandPalette.Empty>            <CommandPalette.Group heading="Suggested">              <CommandPalette.Item id="history" value="Go to History">                Go to History              </CommandPalette.Item>              <CommandPalette.Item id="theme" value="Switch Theme" shortcut="⌘T">                Switch Theme              </CommandPalette.Item>              <CommandPalette.Item id="export" value="Export PDF">                Export PDF              </CommandPalette.Item>            </CommandPalette.Group>            <CommandPalette.Group heading="System">              <CommandPalette.Item id="settings" value="Settings Hub" shortcut=",">                Settings Hub              </CommandPalette.Item>              <CommandPalette.Item id="diagnostics" value="Run Diagnostics">                Run Diagnostics              </CommandPalette.Item>              <CommandPalette.Item id="logout" value="Log Out">                Log Out              </CommandPalette.Item>            </CommandPalette.Group>          </CommandPalette.List>          {/* Childless: the footer renders the canonical Navigate / Select /              Close legend through the shared OverlayHints primitive. */}          <CommandPalette.Footer />        </CommandPalette.Content>      </CommandPalette>    </div>  );}

Examples

Viewfinder frame

Preview

Terminal frame

Preview

Comfortable density

Preview

Dense density

Preview

Tones (manual)

Preview

Tones (auto-coloring)

Preview

Empty state

Preview

API Reference

CommandPalette

NameTypeDefaultDescription
openbooleanfalseControlled open state. Required to actually show the palette — wire a trigger button or shortcut to setOpen(true).
onOpenChange(open: boolean) => voidCalled whenever open state changes.
searchstringControlled search query.
onSearchChange(value: string) => voidCalled when the search query changes.
highlightedstring | nullControlled highlighted item id.
onHighlightChange(id: string | null) => voidCalled when the highlighted item changes.
onActivate(id: string) => voidCalled when an item is activated (Enter or click). Fires after the item's own onSelect.
shouldFilterbooleantrueAuto-filter items by search. Pass false to handle filtering yourself (e.g. server-side).
filter(value: string, search: string) => booleanCustom filter function. Defaults to case-insensitive substring match on the item's value (or id).

CommandPaletteContent

NameTypeDefaultDescription
size"sm" | "md" | "lg""md"Modal width preset.
frame"border" | "viewfinder" | "terminal" | "card" | "none""border"Shell chrome style. "border" renders a 1px hairline. "viewfinder" renders four corner brackets with no border plus a 2px left accent bar on the selected row. "terminal" renders top + bottom 2px rules with inverted selection and swaps the default prefix glyph from > to $. "card" renders an 8px rounded shell with a subtle gradient surface and a floating rounded selection (compose with a search-icon `prefix` for the Linear look). "none" is a bare shell for embedding.
density"compact" | "comfortable" | "dense""compact"Typographic and spacing surface. Switches a token block (--command-palette-row-h, --command-palette-input-py, --command-palette-list-p, --command-palette-text-size, etc.) consumed by every inner slot via [data-density] selectors in command-palette/command-palette.css. "compact" matches the V1 refined-mono target, "comfortable" is Linear-ish breathing room, "dense" is VSCode-tight.
modalbooleantrueRenders the palette as a modal dialog in the browser top layer (default). Pass false to render it in the document flow instead - an embedded palette with the same frame, density, and highlight chrome, without a backdrop, focus trap, or focus restoration. Inline palettes still honour open, so they unmount when the consumer closes them.
labelstring"Command palette"Accessible name for the palette. Applied to the modal dialog, or to the embedded region when modal is false.

CommandPaletteItem

NameTypeDefaultDescription
idrequiredstringStable unique id used for highlight state and aria-activedescendant.
valuestringSearchable text. Defaults to id when omitted.
iconReactNodeOptional leading icon. Inherits tone color when a non-neutral tone is set.
shortcutstringKeyboard shortcut hint rendered next to the label.
tone"neutral" | "nav" | "action" | "settings" | "destructive" | "ai""neutral"Semantic tone. Renders a 2px left bar and tints the optional icon via [data-tone] selectors. The label color stays inherited so contrast holds under any frame. Map: nav→info, action→success, settings→warning, destructive→destructive, ai→accent.
onSelect() => voidCalled when the item is activated. Runs before CommandPalette.onActivate.
disabledbooleanfalseDisable activation and skip in keyboard navigation.

CommandPaletteGroup

NameTypeDefaultDescription
headingReactNodeGroup heading rendered above the items.

CommandPaletteInput

NameTypeDefaultDescription
labelstring"Command search"Accessible label for the search input.
placeholderstring"Type a command…"Search input placeholder.
prefixReactNodeOptional leading content. When omitted, a CSS-driven glyph from --command-palette-prefix-content is rendered (default ">"; terminal frame swaps to "$").
suffixReactNodeOptional trailing content. Defaults to the CommandPaletteCount readout plus an Esc close button; supplying your own suffix replaces both.
closeLabelstring"Close"Accessible name for the default Esc close control — the palette's only touch-reachable close affordance. Set it to localize that name.

Data attributes

AttributeApplies toValuesDescription
data-stateCommandPaletteContent"open" | "closed"Native dialog open state mirrored by the shared shell. Embedded palettes emit "open" while mounted.
data-frameCommandPaletteContent"border" | "viewfinder" | "terminal" | "card" | "none"Shell chrome variant.
data-densityCommandPaletteContent"compact" | "comfortable" | "dense"Typographic and spacing density.
data-valueCommandPaletteItemitem idStable id used for filtering, highlight, and activation.
data-highlightedCommandPaletteItempresent when highlightedMarks the active descendant. All highlight styling keys off this attribute; aria-selected stays for listbox semantics.
data-toneCommandPaletteItem"neutral" | "nav" | "action" | "settings" | "destructive" | "ai"Semantic tone for the optional accent bar and icon tint.
data-emptyCommandPaletteCountpresent when the filter matched nothingSwitches the readout to the error colour alongside CommandPaletteEmpty.

Accessibility

Keyboard Navigation

Arrow keys navigate items (with wrapping), Enter activates the highlighted item. Home and End retain their native search-input editing behavior and do not move the highlight. Escape clears search first, then closes the palette. Hovering (mousemove) over an item also moves the highlight, so mouse and keyboard share a single selection model. Navigation is handled internally via @diffgazer/keys's useNavigation hook.

KeyAction
ArrowUp / ArrowDownMoves highlight through enabled visible items.
EnterActivates the highlighted item.
EscapeClears the search query first, then closes the palette.

Keyboard navigation

Preview

Notes

Requires @diffgazer/keys (package mode)

CommandPalette's arrow-key navigation and focus restoration import from the required @diffgazer/keys peer. Package consumers must install @diffgazer/keys with @diffgazer/ui; packages are not yet published to npm, so until the first release install both from locally packed tarballs. Importing @diffgazer/ui/components/command-palette without keys fails at module load with an error naming the missing @diffgazer/keys package. Copy/dgadd consumers do not need the package — copy mode rewrites the keyboard hooks to local source.

Controlled Open State

CommandPalette is opened from outside via open/onOpenChange. Wire a trigger button (or a global keyboard shortcut) to setOpen(true). Search and highlight state can also be controlled via search/onSearchChange and highlighted/onHighlightChange, or left uncontrolled.

Built-in Filtering

Items are filtered automatically as you type. Each item matches against its `value` prop (falls back to `id`). Pass `shouldFilter={false}` to disable and handle filtering yourself. Pass a custom `filter` function to override the default case-insensitive includes match. Filtering uses the live search value, including the controlled `search` prop when provided.

Composition Contract

Use CommandPalette.Item as an explicit child in the CommandPalette JSX tree, usually inside CommandPalette.List or CommandPalette.Group. Custom item UI belongs inside CommandPalette.Item. Components that create items internally from an opaque wrapper are not part of the current public contract.

Built-in Keyboard Navigation

CommandPalette integrates @diffgazer/keys's useNavigation internally for arrow-key navigation, wrapping, and Enter activation. Hover (mousemove) and keyboard selection share the same highlighted state, so mousing over an item moves the cmdk-style cursor. Highlight and search state can be controlled externally via highlighted/onHighlightChange and search/onSearchChange.

Variants & Density

CommandPaletteContent exposes two orthogonal axes. `frame` picks the shell chrome — "border" (1px hairline, default), "viewfinder" (no border, four corner brackets + 2px left accent bar on selection), "terminal" (top + bottom 2px rules, inverted selection, prefix glyph becomes $), "card" (rounded 8px shell with a subtle gradient surface and floating rounded selection — Linear-ish), or "none" (bare shell for embedding). `density` picks the typographic & spacing surface — "compact" (default), "comfortable", or "dense". Both are plain TypeScript types (CommandPaletteFrame, CommandPaletteDensity) whose visual styling is driven by [data-frame] / [data-density] selectors in command-palette/command-palette.css, so consumers can override token values per-instance via CSS custom properties.

Modal or Embedded

By default Content renders a native modal dialog in the browser top layer with a focus trap and focus restoration. Pass modal={false} to embed the same surface in the page: identical frame, density, tone, and highlight chrome, but in the document flow, without a backdrop and without stealing focus - useful for a persistent search pane, and for documenting the open surface itself. The examples below are embedded palettes for exactly that reason.

Keyboard legend

CommandPaletteFooter renders the shared OverlayHints primitive (registry/ui/shared/overlay-hints) when given no children: Navigate / Select / Close, in one layout every keyboard-first overlay in the library speaks. Pass children to take full control. The legend is aria-hidden — the shortcuts are already reachable through the real controls — and at coarse pointer every hint that is not marked touch-relevant is hidden, collapsing the whole bar rather than leaving an empty strip; on a 390-wide palette that recovers a full row, and the working close affordance is the Esc button in the input row.

Narrow-viewport geometry

Below 640px the panel insets 12px from each viewport edge (max-sm:mx-3 plus a matching width) so both vertical hairlines and the offset shadow stay on-screen instead of being clipped. The panel is top-pinned at every width — margin-block-start: max(12px, env(safe-area-inset-top)) with an auto bottom margin — so the software keyboard shrinking the visual viewport cannot displace the input row mid-typing. The height cap stays max-h-[80dvh]. The footer pads past the home indicator with env(safe-area-inset-bottom), which needs viewport-fit=cover on the host page and degrades to the base padding without it. The Esc affordance in the input row is a real button with the accessible name "Close" and a 44x44 hit area expanded via ::before, so touch users have a working close control; the visible chip does not move.

Position readout

The default input suffix renders CommandPaletteCount: a bracketed readout of the highlighted position over the filtered total — [3/24] with a highlight, [24] without one, and [0] with a data-empty marker (error colour) when the filter matched nothing. It is aria-hidden, because the palette's existing polite live region already announces the result count; rendering it visibly is what closes the gap for sighted keyboard users. Supplying your own `suffix` replaces it, so compose CommandPalette.Count yourself if you want both.

Optional auto-coloring

Items accept a `tone` prop ("neutral" | "nav" | "action" | "settings" | "destructive" | "ai") that renders a 2px left accent bar and tints the optional icon. The label color is unchanged so contrast remains readable, including under the terminal frame's inverted selection. For automatic classification + inline match highlighting, import `CommandPaletteHighlightItem` from `@diffgazer/ui/components/command-palette/highlight`. It infers tone from a small regex table (verbs like "delete", "go to", "toggle", "ask", "run") and wraps matched characters in `<mark data-slot="command-palette-item-match">`.

Source

Install via CLI: pnpm exec dgadd add ui/command-palette. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.