Kanzo UI
Actions

Command

A filterable command palette, inline or in a dialog.

Ark UI

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.

Post a contract⌘N
Claim a contract
Show overdue contracts
Find a member⌘K
Who is ready today
Open the bestiary
Edit the party rules
Change the hall's colours

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.

AdaSmiths
BrannocSmiths
CiraScribes
DovScribes

Keyboard

KeyDoes
↓ / ↑Moves the highlight to the next / previous command, stopping at the ends rather than wrapping.
Home / EndHighlights the first / last command — the list owns them outright, because the palette is never closed.
EnterRuns the highlighted command; the list stays open and the query clears.
EscCloses 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

PropTypeDefault
hotkeystring — "mod+k", or a bare key—

The rest is Ark's Dialog.Root. Its props type is CommandDialogProps.

CommandDialogContent

PropTypeDefault
titlestring"Command Palette"
descriptionstring"Search for a command to run..."
sizeDialogContentProps["size"]"lg"

CommandInput

PropTypeDefault
sizeInputProps["size"]"md"
childrenReactNode — drawn in the field, before the input—

CommandGroup

PropTypeDefault
headingstring | ReactNode—

collection, onInputValueChange, onValueChange and the rest are Ark's Combobox.Root; CommandItem takes Ark's item.

On this page