Menus
Command
A command palette: a searchable list of every action in the app, in a dialog. Typing filters the commands by label and keywords (ignoring case and accents), the arrow keys move the highlight, and Enter runs the highlighted command. Commands come in labelled groups and show their shortcuts with Kbd; when nothing matches it says so. Below 640px it is a full-height bottom sheet, whose search input stays put while the results filter.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/command#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/commandIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/command#v1.2.13 ourostack/ouro-ui/command-tests#v1.2.13import { Command } from "@/components/ouro/command/command";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
const [open, setOpen] = useCommandShortcut("mod+k");
<Command
title="Command palette"
open={open}
onOpenChange={setOpen}
items={[
{ label: "Recipes", items: [{ label: "New recipe", onSelect: create, shortcut: ["mod", "n"] }] },
{ label: "Switch theme", onSelect: toggleTheme, keywords: ["dark mode"] },
]}
/>Usage
Open it with trigger, or control it with open and onOpenChange, usually from useCommandShortcut, which opens it on ⌘K (Ctrl+K elsewhere): import { useCommandShortcut } from "@/lib/ouro/use-command-shortcut".
useCommandShortcut(shortcut = "mod+k") returns [open, setOpen] and toggles open on the shortcut anywhere on the page. It takes the keys as "mod+k" or ["mod", "k"], with Kbd's names: mod (⌘ on Apple platforms, Ctrl elsewhere), cmd, ctrl, alt and shift. It needs exactly those modifiers, ignores a held key's repeats and prevents the browser's action for that combination. It never fires while an input method is composing text, and in a text field it fires only when the shortcut includes mod, cmd or ctrl, so a shortcut like "/" never takes a typed character.
Use instead when
- Use
Menuinstead for the few actions on one thing, andComboboxto pick a value for a form.
Props
The props the modes share. Type: CommandProps.
className- Type:
string - Layout only, applied to the dialog panel: margins (
mt-8). Appearance classes warn in development. closeLabel- Type:
string - Default:
"Close" - The label of the close button, hidden until it has keyboard focus, so screen-reader users on phones (who have no Escape key) can always close the palette.
emptyLabel- Type:
string - Default:
"No results" - What shows when nothing matches the search.
items(required)- Type:
readonly CommandEntry[] - The commands: groups (
{ label: "Recipes", items: [...] }), single commands, or both. loading- Type:
boolean - Whether results are on their way: shows
loadingLabelbeside the input, announces it politely for the first load of each opening (not again while typing goes on), and holds back the empty state. Set it back to false when the request fails or aborts. loadingLabel- Type:
string - Default:
"Searching…" - What shows, and is announced, while
loading. onQueryChange- Type:
((query: string, context: { signal: AbortSignal }) => void) - For results from a server: called with the search text on every change, and with the current text (
""at each opening) whenever the palette opens or swaps between dialog and sheet. The palette then stops filteringitemsitself and shows them as they are, so keep the last results initemsuntil new ones arrive.signalaborts when a newer query replaces this one or the palette closes: pass it tofetch, or drop the response whensignal.aborted, so a slow earlier response never overwrites a newer one. The app owns the request, and must setloadingback to false when the request fails or its signal aborts, or "Searching…" stays up. placeholder- Type:
string - Default:
"Type a command or search…" - The search input's placeholder.
searchLabel- Type:
string - Default:
"Search commands" - The search input's label for screen readers.
title(required)- Type:
string - Names the palette for screen readers; it is not shown. It is always required.
Modes
CommandProps is a union: pass the props of exactly one mode. Mixing them is a type error.
Controlled: CommandControlledProps
A controlled Command palette, usually opened by useCommandShortcut.
onOpenChange(required)- Type:
(open: boolean) => void - Called with
falsewhen the user closes the palette (Escape, outside press, swipe), and when a command runs. open(required)- Type:
boolean - Whether the palette is open. After a key press it focuses the search input; after a touch or pen tap it focuses itself, so no software keyboard pops up.
Not in this mode (typed never):
trigger: Not withopen. RemoveopenandonOpenChangeto let a trigger open the palette.
With trigger: CommandWithTriggerProps
An uncontrolled Command palette: trigger opens it.
trigger(required)- Type:
ReactElement - The element that opens the palette, usually a Button:
trigger={<Button>Search</Button>}.
Not in this mode (typed never):
onOpenChange: Not withtrigger. Removetriggerto control the palette withopenandonOpenChange.open: Not withtrigger. Removetriggerto control the palette withopenandonOpenChange.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
children: Not a prop. The commands areitems:items={[{ label: "New recipe", onSelect: create }]}.commands: Not a prop. The commands areitems:items={[{ label: "New recipe", onSelect: create }]}.isOpen: Not a prop. Ouro UI usesopenwithonOpenChange:<Command open={open} onOpenChange={setOpen}>.onClose: Not a prop.onOpenChange(false)fires on close:<Command open={open} onOpenChange={setOpen}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
CommandEntry
One entry in a Command palette's items: a command, or a group of them.
type CommandEntry = CommandItem | CommandGroup;CommandGroup
A labelled group of commands in a Command palette's items.
interface CommandGroup {
/** The group's heading: "Recipes". */
label: string;
/** The group's commands. */
items: readonly CommandItem[];
}CommandItem
One command in a Command palette's items.
interface CommandItem {
/** The command's text, which the search matches. */
label: string;
/** Runs the command. The palette closes first. */
onSelect: () => void;
/**
* More words the search matches, which never show: `keywords: ["dark mode"]`.
*/
keywords?: readonly string[] | undefined;
/** The keys of the command's shortcut, shown at its end with `Kbd`: `["mod", "n"]`. It does not bind them. */
shortcut?: readonly string[] | undefined;
/** Listed, but cannot be run. */
disabled?: boolean | undefined;
/** Not a prop. A command runs `onSelect`: `{ label: "New recipe", onSelect: create }`. (A `MenuItem` is the one that takes `onClick`.) */
onClick?: { 'Use onSelect: { label: "New recipe", onSelect: create }': never };
}