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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

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

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/command

Its 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.13
import { 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 Menu instead for the few actions on one thing, and Combobox to 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 loadingLabel beside 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 filtering items itself and shows them as they are, so keep the last results in items until new ones arrive. signal aborts when a newer query replaces this one or the palette closes: pass it to fetch, or drop the response when signal.aborted, so a slow earlier response never overwrites a newer one. The app owns the request, and must set loading back 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 false when 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 with open. Remove open and onOpenChange to 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 with trigger. Remove trigger to control the palette with open and onOpenChange.
  • open: Not with trigger. Remove trigger to control the palette with open and onOpenChange.

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 are items: items={[{ label: "New recipe", onSelect: create }]}.
  • commands: Not a prop. The commands are items: items={[{ label: "New recipe", onSelect: create }]}.
  • isOpen: Not a prop. Ouro UI uses open with onOpenChange: <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 };
}