Command
A filterable command palette, inline or in a dialog.
A palette runs commands, so why is it a combobox? Because the role follows the interaction, not the payload: type to filter, arrow to highlight, enter to run — and a menu cannot filter at all. See the rule.
Usage
import {
Command,
CommandContent,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
useFilter,
useListCollection,
} from "@kanzo-tech/ui";Command is a Combobox that is permanently open, never closes on select, and clears its
selection after each pick — the behaviours a palette needs and a combobox does not. You drive
it with a list collection and filter its items on every keystroke:
const { contains } = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: COMMANDS,
filter: contains,
groupBy: (item) => item.group,
});Filtering happens in your collection, not inside the component, so the same palette can be fed by a local array or a server search without changing its markup.
Anatomy
<Command collection>
<CommandInput />
<CommandContent>
<CommandEmpty />
<CommandList>
<CommandGroup heading>
<CommandItem item>
<CommandShortcut />
<CommandSeparator />
<CommandFooter />The root is flex-1 with min-h-0, so it needs a parent that gives it a height — a flex
column of fixed height inline, or CommandDialogContent.
In a dialog
CommandDialog is the Dialog root plus the one thing a palette adds: hotkey, the key that
opens and closes it. "mod+k" is ⌘+K or Ctrl+K — the
spelling of Mantine's useHotkeys, and the same grammar PreferencesRoot takes. It is opt-in: a
design system claims a key in its host's keymap only when asked, and a key pressed while typing in a
field is the field's. Controlled or not, the key goes through the dialog's own machine, so
onOpenChange hears it.
CommandDialogContent supplies the sizing plus a screen-reader-only title and description, so the
palette is announced without visible chrome, and the list fills its height. Because the root is
Dialog's, so is the trigger: DialogTrigger asChild over a Button that prints the key, as on
Shark's own site.
With filters as chips
CommandInput draws its children in the field before the input, as ComboboxInput does. A
palette that filters puts chips there and lets one input belong to two machines, the
composition of Shark's tags-input/example-combobox: wrap the palette in TagsInputRootProvider,
share the input's id between the two, and spread the tags-input's input props first, so the
combobox's own id and value win and both hear every key.
const tags = useTagsInput({ ids: { input: id }, delimiter: " ", validate, onValueChange: () => setText("") });
// The machine's props are typed as an `<input>`'s, whose `size` is a number; here `size` is the field's.
const keys: Omit<ReturnType<typeof tags.getInputProps>, "size"> = tags.getInputProps();
<TagsInputRootProvider value={tags}>
<Command ids={{ input: id }} inputValue={text} onInputValueChange={…}>
<CommandInput {...keys} placeholder="…">
{tags.value.map((value, index) => <TagsInputItem index={index} key={value} value={value}>…</TagsInputItem>)}
</CommandInput>validate decides what a word must be to become a chip, so free text stays text. The keys are the
two machines' own: Space after a valid word makes a chip, Enter runs the
highlighted item and makes a chip only when nothing is highlighted, and Backspace in an
empty field highlights the last chip, then removes it.
Keyboard
| Key | Does |
|---|---|
| ↓ / ↑ | Moves the highlight to the next / previous command, stopping at the ends rather than wrapping. |
| Home / End | Highlights the first / last command — the list owns them outright, because the palette is never closed. |
| Enter | Runs the highlighted command; the list stays open and the query clears. |
| Esc | Closes CommandDialog; an inline palette has nothing to close. |
Command sets inputBehavior="autohighlight", so the top match is already highlighted after
every keystroke and Enter runs it without a single ↓. No global shortcut is
bound unless you pass hotkey.
API Reference
CommandDialog
| Prop | Type | Default |
|---|---|---|
hotkey | string — "mod+k", or a bare key | — |
The rest is Ark's Dialog.Root. Its props type is CommandDialogProps.
CommandDialogContent
| Prop | Type | Default |
|---|---|---|
title | string | "Command Palette" |
description | string | "Search for a command to run..." |
size | DialogContentProps["size"] | "lg" |
CommandInput
| Prop | Type | Default |
|---|---|---|
size | InputProps["size"] | "md" |
children | ReactNode — drawn in the field, before the input | — |
CommandGroup
| Prop | Type | Default |
|---|---|---|
heading | string | ReactNode | — |
collection, onInputValueChange, onValueChange and the rest are Ark's Combobox.Root;
CommandItem takes Ark's item.