Skip to content

Command

A ⌘K command palette: a modal search field that filters commands and destinations as you type.

Preview

Installation

pnpm add @syntara/react @syntara/tokens
import { CommandDialog, CommandItem, CommandSection, useCommandShortcut } from '@syntara/react';

Usage

import { useState } from 'react';
import { Button, CommandDialog, CommandItem, CommandSection, useCommandShortcut } from '@syntara/react';

export function Search() {
  const [isOpen, setOpen] = useState(false);
  useCommandShortcut(() => setOpen(true));
  return (
    <>
      <Button variant="outline" onPress={() => setOpen(true)}>Search…</Button>
      <CommandDialog isOpen={isOpen} onOpenChange={setOpen} placeholder="Search docs…" onAction={(key) => navigate(`/components/${key}`)}>
        <CommandSection title="Components">
          <CommandItem id="button">Button</CommandItem>
          <CommandItem id="dialog" textValue="Dialog modal">Dialog</CommandItem>
        </CommandSection>
      </CommandDialog>
    </>
  );
}

Examples

Dynamic items

Render results from an items array; textValue adds searchable keywords.

Accessibility

KeysAction
⌘KorCtrlKOpens the palette (with useCommandShortcut).
TypingFilters the results; the first match becomes active.
↑or↓Moves the active result while focus stays in the input.
EnterRuns the active result and closes the palette.
EscapeClears the query; a second Escape closes the palette and restores focus.
  • The input is a combobox (WAI-ARIA APG pattern): it controls a role="listbox" of options via aria-controls and points at the active option with aria-activedescendant, so focus never leaves the input. The results list scrolls without being a tab stop; the combobox relationship is what makes that keyboard-accessible (axe scrollable-region-focusable passes).
  • The dialog and input are named by aria-label.
  • Overlays render in a portal on <body>, outside any ThemeScope. When it opens, the overlay looks up data-syntara-theme, data-syntara-scheme, data-syntara-density, dir and lang — each on the nearest ancestor that has it — starting from its trigger (or from where the component is rendered, when controlled), and copies them onto its own root — so it uses the same tenant tokens and direction as the page region it came from, and nested overlays inherit from their parent overlay.
  • The backdrop blurs the page lightly and dims it with backdrop-filter: brightness(0.6), so it recedes in light and dark schemes alike. The panel is glass (--syntara-glass-bg + backdrop blur); only text.default and text.subtle sit directly on it, the two roles the theme engine solves to 4.5:1 over any backdrop.
  • Focus indicator: the hairline under the search field becomes a 2px focus-ring line while the input has focus.
  • Motion: the panel fades in and springs up from 0.96 scale; the highlighted row's background blooms in and fades out as the arrow keys move, with a 2px focus-ring bar at its inline start so the active row is marked by shape as well as fill. With prefers-reduced-motion nothing moves; the highlight only fades.

Guidelines

Do

  • Group results into a few titled sections.
  • Put synonyms in textValue so people find items by the words they use.
  • Show the shortcut near the search button so people learn it.

Don’t

  • Don't put hundreds of static items in it; filter on the server and pass items.
  • Don't use it as the only way to reach a page.

API reference

CommandDialog

children / itemsRequired
ReactNode | (item) => ReactElement / Iterable<T>

CommandSection and CommandItem elements, or a render function over items.

isOpen / defaultOpen / onOpenChange
boolean / boolean / (isOpen: boolean) => void

Open state. Omit when the palette is inside a DialogTrigger.

onAction
(key: Key) => void

Called with the chosen item's id on Enter or click; the palette then closes.

placeholder
string

Placeholder of the search input.

Default 'Type a command or search…'

aria-label
string

Accessible name of the palette and its input.

Default 'Command menu'

filter
(textValue: string, inputValue: string) => boolean

Decides whether an item matches the query.

Default case- and accent-insensitive contains

renderEmptyState
(inputValue: string) => ReactNode

Shown when nothing matches.

Default No results for “…”

footer
ReactNode

Replaces the keyboard hint row; null hides it.

className / style
string / CSSProperties

Applied to the palette panel.

CommandSection

title
ReactNode

Group heading. Sections with no matching items are hidden.

CommandItem

childrenRequired
ReactNode

The result label.

textValue
string

Text the query is matched against. Defaults to children when it is a string; add synonyms here.

icon
ReactNode

Leading icon. Decorative.

description
ReactNode

Second line under the label.

meta
ReactNode

Short trailing text at the inline end, e.g. a category or shortcut.

useCommandShortcut

onOpenRequired
() => void

Called on ⌘K (macOS) or Ctrl+K anywhere on the page, unless an earlier listener already called preventDefault() on the key press.

options
{ key?: string; isDisabled?: boolean }

Bind another letter or turn the shortcut off.

Default { key: 'k' }

Tokens

The semantic tokens this component reads, grouped by what they control. Swatches show this site’s theme; change a tenant’s brand and the component follows with no code change.

Colour9
text.defaultborder.defaultsurface.raisedborder.subtlefocus.ringtext.subtlesurface.selectedtext.disabledsurface.sunken
Type10
font.bodyfont.size.mdline-height.normalfont.size.xsfont.weight.mediumline-height.snugfont.size.smfont.monofont.tracking.mdfont.tracking.xs
Space and size8
space.16space.4space.2control-heightspace.1space.3space.8space.5
Shape2
radius.containerradius.badge
Depth4
glass.blurshadow.overlayhairlineglass.opacity
Motion6
motion.duration.fastmotion.easingmotion.duration.springmotion.springmotion.duration.normalmotion.easing-out
Other2
sheenrim