Command Palette

Pro

Spotlight-style launcher with light/dark variants, 3 sizes, fuzzy search, ⌘K shortcut, grouped results, query highlighting, recent history, async/remote loading, disabled commands and keyboard navigation.

</>sv-command-palette commandsearchshortcutvariantfuzzy
import { SvCommandPaletteComponent } from '@styloviz-pro/command-palette';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Remote search without the race

Pro

Cancelling the search the user typed past

Pro

Dark variant, large, fuzzy

Pro

Custom shortcut & no recents

Pro

Remote/async results

Pro

When to use

  • Giving power users a fast keyboard launcher for actions and navigation.
  • Apps with many commands where menus would be unwieldy.
  • Remote/async command sources — drive `[loading]` from `queryChange`.
  • For searching data records specifically, use the Search Bar.

Async states

  • loading
  • error
  • retry

Item identity & owned state

Restorable state
  • recentCommandIds

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
groupsCommandGroup[][]Command groups to display. Provide all available commands here.
configCommandPaletteConfig{}Optional configuration for placeholder, empty message and recent limit.
portalClassstring''Classes applied to the body-portal container. WITHOUT THIS, A HOST THAT SCOPES ITS THEME OR ITS TOKENS CANNOT THEME THE PALETTE AT ALL. The overlay is instantiated into `document.body` so its fixed backdrop escapes any ancestor `overflow`/`transform`/`filter` containment — which is correct, and also means it escapes every scoped class on the host. Two things break as a result, and both are invisible until someone opens the palette: • `.dark` on a section root (the documented way to force dark inside a light page) no longer reaches it, so the palette opens WHITE on a dark page; • a `.tpl-*` token scope no longer reaches it either, so a themed template's palette highlights its selected row in the kit's default blue instead of the template's own accent. Pass the scopes the palette should inherit, e.g. `portalClass="dark tpl-dev"`.
variantCommandPaletteVariant'default'Panel theme.
sizeCommandPaletteSize'md'Panel width preset.
placementCommandPalettePlacement'top'Vertical anchoring of the panel.
loadingbooleanfalseExternal loading flag — set true while a parent fetches remote results (drive it from `queryChange`). Shows a loading row in the results area.
serverSidebooleanfalseLet the server do the matching. The supplied `groups` are shown exactly as given — a palette over a large workspace searches documents and people, not just the commands already in the browser.
debounceMsnumber200Delay before `queryChange` fires, in ms. `0` emits immediately. A palette wants this short — it is the primary interaction, not a filter.
minCharsnumber0Characters required before a remote search is sent. Below this the palette says so instead of "No results found".
errorSvDataError | string | nullnullFailure loading commands, rendered in place of the list.
resultsForstring | nullnullWhich term the supplied `groups` are the answer to. WITHOUT THIS THE PALETTE CANNOT KNOW ITS RESULTS ARE STALE, AND IS EXACTLY THAT. The host answers "de" and then "del"; the slower "de" response lands second and the list shows commands for a term the user has already typed past. Nothing in the markup looks wrong, and the palette has no way to tell — `groups` is just an array. AND IT IS WORSE THAN COSMETIC. The highlighted row is whatever the stale list put first, so Enter runs a command the user never saw offered for what they typed. Set this and the palette keeps saying "Searching…", marks the listbox `aria-busy`, refuses to run a command from a list it knows is out of date, and — the quiet one — does NOT say "No results found" about an answer to a different question. Echo back the `term` the palette handed you. Only consulted with `serverSide`.
canClose((reason: SvCloseReason) => SvCloseVerdict) | nullnullHost veto over a USER-initiated close — Escape or the backdrop. THE ONE FAMILY NAMED THAT HAD NO VETO. Modal, Drawer and Confirm Dialog all take `canClose`; the palette dropped a half-typed multi-step command on the first stray backdrop click. Return `false` to keep the palette open; `closeCancelled` reports the refusal. `close()` stays unconditional: a host calling it IS the decision, the same rule `createCloseGuard` applies everywhere (`programmatic` is always allowed).
recentCommandIdsreadonly string[] (two-way)[]IDs of recently selected commands, most-recent first (two-way). A `model` because a most-recently-used list that only lives in the component is reset by every page load — the one thing an MRU list must survive. Bind it to restore what you stored, and read `(recentCommandIdsChange)` to store it. IDs that no longer match a command in `groups` are ignored, so a stale saved list is safe to hand back verbatim.

Outputs

EventPayload typeDescription
openedvoidEmitted when the palette opens.
closedvoidEmitted when the palette closes (any method).
closeCancelledSvCloseReasonA user close (Escape / backdrop) was vetoed by `canClose`.
queryChangestringEmitted whenever the search query changes (debounce/fetch in the parent).
searchRequestCommandPaletteSearchThe same change, with a token and an `AbortSignal`. Emitted immediately after `queryChange`, for hosts that want to cancel the request the user has typed past rather than let it finish and be discarded. Ignore it and nothing changes.
commandSelectedCommandActionEmitted when a command is confirmed (before its action runs).
commandError{ command: CommandAction; error: unknown }Emitted when an async command's action rejects.
retryvoidThe host should re-run the failed command load.
shortcutConflictsreadonly CommandShortcutConflict[]Two or more commands declare the same `keys`. REPORTED WHETHER OR NOT ANYTHING IS BOUND. Duplicate shortcuts were "undetected" because `shortcut` was a list of glyphs nobody parsed — two commands could claim ⌘K and the palette would draw both. Detection is a pure check over the command list, so it does not wait for `bindCommandKeys`: two commands claiming one binding is a bug in the list either way.
recentCommandIdsChangereadonly string[]Emits the new `recentCommandIds` whenever it changes — the output half of `[(recentCommandIds)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — it just saves you writing the debounce, the request guard and the retry policy by hand.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

One controller handles debounce, request-guarding, cancellation and retry.

  • `queryChange` carries an `SvDataQuery` — page, page size, sorts, filters, search and cursor in one object. One handler covers every way the user can change what they are asking for.
  • `serverSide` switches **off** the built-in filtering, sorting and paging. Leave it false and the component works entirely from the array you pass — which is what you want for a few hundred rows.
  • `debounceMs` (default 300) collapses a burst of typing into one request. Set it to 0 only when the query is driven by clicks.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Light/dark theme variants, three sizes (sm · md · lg), and top/center placement.
  • Grouped, searchable commands with substring or fuzzy (subsequence, ranked) matching; recent history and badges.
  • Configurable/optional global shortcut, `showRecent` toggle, disabled and keep-open commands.
  • The open shortcut goes through the shared registry (`X-15`): `config.shortcutScope` puts it in the global, route or overlay band — the listener used to be bound unconditionally to `document`, so a palette inside a dialog competed with the app's own on equal terms — and a second component claiming the same binding is refused and reported rather than firing alongside it.
  • A command's `keys` (e.g. `'mod+shift+p'`) is a real binding: it renders itself for the running platform, colliding ones are reported through `(shortcutConflicts)`, and `config.bindCommandKeys` registers them so pressing one runs the command through the same path a click takes. `shortcut` remains the display-only glyph array.
  • Rendered into `document.body` via a portal, so it always covers the full viewport and is never clipped by a parent `overflow`/`transform`.
  • Return a `Promise` from a command `action` for an inline loading spinner; remote sources use `[loading]` + `queryChange`; failures keep the palette open with an error state and emit `commandError`.

Best practices

  • Do: Register the ⌘K/Ctrl+K shortcut and hint it in the UI (customise with `config.shortcutKey`).
  • Do: Group commands and surface recents so frequent actions are one keystroke away.
  • Do: Enable `config.fuzzy` for forgiving, ranked matching on large command sets.
  • Tip: Keep command labels verb-first ("Create invoice") for scannability.
  • Do: Give a command `keys` rather than a `shortcut` glyph array. `keys` is a binding: it renders itself as ⌘⇧P or Ctrl Shift P for the platform the user is on, and two commands claiming the same one are reported through `(shortcutConflicts)`. The glyph array binds nothing and cannot be checked against anything.
  • Do: Set `config.shortcutScope` to `'overlay'` for a palette that lives inside a dialog, so its ⌘K outranks the app's while the dialog is open and hands it back on close.
  • Tip: Turn on `config.bindCommandKeys` only if the app does not already bind its commands itself — otherwise every shortcut fires twice.
  • Don't: Don't use a key the browser keeps for itself — ⌘/Ctrl + N, T, W, Q, Tab, or Shift + N/T/W — as the open key or a command key. The page never receives it in a browser tab; the palette warns about it in dev mode.

Accessibility

  • Combobox semantics with `aria-activedescendant` on the search input tracking the active result; `role="listbox"`/`option` results with `aria-selected` and `aria-disabled`.
  • Full keyboard control (↑↓ skip disabled, Enter, Esc, Tab); focus returns to the prior element on close — and if a command removes that element (a navigation tearing down the old view), focus moves to a surviving neighbour or the main landmark once the exit transition ends, instead of `<body>`. Opened with nothing focused, it hands focus to the main landmark.
  • With `resultsFor`, rows that answer an older term — including during the debounce window, before the new term has been requested — stay on screen dimmed but `inert`, are dropped from `aria-activedescendant`, and cannot be run.
  • Running commands set `aria-busy`; async failures are announced via an assertive live region.

Feedback on Command Palette

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.