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>
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.