Command palette

A recipe, not a package namespace. Dialog holds the palette, and useCombobox owns the input, filtering, the active option and ids. There is no CommandPalette.* export.

Use the dialog presentation and spread getDialogProps() on Dialog.Root. When the palette opens, the hook clears the input and makes the top row active, and a command chosen while the palette closes does not run again. Keep value at null and run the command in onValueChange, so choosing one leaves nothing selected.

const cb = useCombobox({
  options: commands,
  presentation: 'dialog',
  value: null,
  onValueChange: (value: string | null) => { if (value) run(value) },
})
useHotkey(['Control+K', 'Meta+K'], () => !cb.open && cb.setOpen(true))

<Button.Root {...cb.getTriggerProps()} aria-keyshortcuts="Control+K Meta+K">
  Search commands
</Button.Root>
<Dialog.Root {...cb.getDialogProps()} aria-label="Commands">
  <Field.Root id={cb.ids.input} aria-label="Command">
    <Field.TextInput {...cb.getInputProps()} placeholder="Type a command" />
  </Field.Root>
  <OptionList.Root {...cb.getListboxProps()} hidden={cb.isEmpty}>
    {cb.items.map((item) => (
      <OptionList.Option key={item.id} {...cb.getOptionProps(item)} />
    ))}
  </OptionList.Root>
  <Text role="status">{cb.isEmpty ? 'No matching commands.' : ''}</Text>
</Dialog.Root>
Create invoice
Add customer
Open billing settings
Switch theme
Sign out

Press Ctrl+K or ⌘K, or use the button.

The shortcut is useHotkey, which opens the palette on Ctrl+K or ⌘K. aria-keyshortcuts on the trigger tells assistive technology about it. Escape and a click on the backdrop close the dialog, and focus goes back to the element that had it when the palette opened, or to the trigger when nothing had focus. The empty message sits in a role="status" region, so screen readers announce it.

When to use

When: One place to search and run actions across an app.

When not: Picking a value for a form field → the Combobox recipe. A short, fixed list of actions → Menu.