Command
A ⌘K command palette: a modal search field that filters commands and destinations as you type.
Preview
Installation
pnpm add @syntara/react @syntara/tokensimport { 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
| Keys | Action |
|---|---|
| ⌘KorCtrlK | Opens the palette (with useCommandShortcut). |
| Typing | Filters the results; the first match becomes active. |
| ↑or↓ | Moves the active result while focus stays in the input. |
| Enter | Runs the active result and closes the palette. |
| Escape | Clears 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 / itemsRequiredReactNode | (item) => ReactElement / Iterable<T>CommandSection and CommandItem elements, or a render function over items.
isOpen / defaultOpen / onOpenChangeboolean / boolean / (isOpen: boolean) => voidOpen state. Omit when the palette is inside a DialogTrigger.
onAction(key: Key) => voidCalled with the chosen item's id on Enter or click; the palette then closes.
placeholderstringPlaceholder of the search input.
Default
'Type a command or search…'aria-labelstringAccessible name of the palette and its input.
Default
'Command menu'filter(textValue: string, inputValue: string) => booleanDecides whether an item matches the query.
Default
case- and accent-insensitive containsrenderEmptyState(inputValue: string) => ReactNodeShown when nothing matches.
Default
No results for “…”footerReactNodeReplaces the keyboard hint row; null hides it.
className / stylestring / CSSPropertiesApplied to the palette panel.
CommandSection
titleReactNodeGroup heading. Sections with no matching items are hidden.
CommandItem
childrenRequiredReactNodeThe result label.
textValuestringText the query is matched against. Defaults to children when it is a string; add synonyms here.
iconReactNodeLeading icon. Decorative.
descriptionReactNodeSecond line under the label.
metaReactNodeShort trailing text at the inline end, e.g. a category or shortcut.
useCommandShortcut
onOpenRequired() => voidCalled 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