# Command Menu

> A React command menu (⌘K palette) with groups, shortcuts, nested pages, async results and recents, and a full-height sheet on phones.

- Type: React component (Navigation)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add command-menu`
- Docs: https://wingo-ui.com/components/command-menu

## Interaction

Press ⌘K (Ctrl+K elsewhere) or click the search field to open it near the top of the screen. Type to filter: case and diacritics never matter and the matched letters turn bold; results from the server appear under the commands with a spinner in the input while they load. Arrow keys (and Ctrl+N / Ctrl+P) move, Home / End jump, Enter runs, and an item's own shortcut runs it while the menu is open. Items with a chevron open a page: its title appears as a chip before the input, Backspace on an empty input or Escape goes back, and Escape on the first page closes. Recently picked items come first next time (server results for the rest of the session). On a phone it opens as a full-height sheet with the keyboard already up, 48px rows and a Cancel button; swipe it down to close.

## Installation

```bash
npx wingo-ui@latest add command-menu
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/command-menu` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/command-menu.tsx`

npm dependencies: `npm i @radix-ui/react-dialog @radix-ui/react-visually-hidden cmdk lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useDebounce](https://wingo-ui.com/components/use-debounce) ([Markdown](https://wingo-ui.com/components/use-debounce.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
"use client"

import { FilePlus2, Moon, Sun, UserPlus } from "lucide-react"
import { useTheme } from "next-themes"
import { CommandMenu, CommandMenuTrigger, commandItemsFromNav, type CommandGroup } from "@/components/ui/command-menu"
import type { NavItem } from "@/lib/navigation"

const nav: NavItem[] = [
  { label: "Clients", href: "/clients" },
  { label: "Invoices", href: "/invoices", items: [{ label: "Overdue", href: "/invoices/overdue" }] },
]

export function AppCommandMenu() {
  const { setTheme } = useTheme()
  const groups: CommandGroup[] = [
    { label: "Go to", items: commandItemsFromNav(nav) },
    {
      label: "Actions",
      items: [
        { id: "new-client", label: "New client", icon: <UserPlus />, href: "/clients/new" },
        { id: "new-invoice", label: "New invoice", icon: <FilePlus2 />, shortcut: "shift+mod+f", href: "/invoices/new" },
        { id: "theme", label: "Theme…", icon: <Moon />, page: "theme" },
      ],
    },
  ]
  return (
    <CommandMenu
      groups={groups}
      pages={[
        {
          id: "theme",
          title: "Theme",
          groups: [
            {
              items: [
                { id: "light", label: "Light", icon: <Sun />, onSelect: () => setTheme("light") },
                { id: "dark", label: "Dark", icon: <Moon />, onSelect: () => setTheme("dark") },
              ],
            },
          ],
        },
      ]}
      loadItems={async (query, { signal }) => {
        const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal })
        const clients: { id: string; name: string; city: string }[] = await res.json()
        return [{ label: "Clients", items: clients.map((c) => ({ id: c.id, label: c.name, description: c.city, href: `/clients/${c.id}` })) }]
      }}
      placeholder="Type a command or search…"
      trigger={<CommandMenuTrigger placeholder="Search or jump to…" />}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `groups?` | `CommandGroup[]` |  | The root page: groups of { id?, label?, items }. An item is { id, label, icon?, description?, badge?, keywords?, shortcut?, href?, external?, page?, onSelect?, tone?, disabled?, closeOnSelect? }. Build a “Go to” group from your navigation with commandItemsFromNav (see Parts). |
| `pages?` | `CommandPage[]` |  | Pages pushed by items with page: { id, title, placeholder?, groups?, loadItems? }. A page's own groups and loader replace the root ones. |
| `trigger?` | `ReactNode` |  | Rendered asChild as the trigger. CommandMenuTrigger (see Parts) is the ready-made search-field button. |
| `loadItems?` | `(query: string, context: { signal: AbortSignal; page: string \| null }) => Promise<CommandGroup[]>` |  | Async results shown after the static groups (clients, invoices). Debounced, and the previous request is aborted. On the root page it runs once the query is not empty; errors show a row with Try again. |
| `debounce?` | `number` | `200` | Milliseconds of quiet typing before loadItems runs. |
| `defaultOpen?` | `boolean` | `false` | Starts open. Use open and onOpenChange to control it. |
| `open?` | `boolean` |  | Controlled open state; pair it with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the menu opens or closes. |
| `page?` | `string \| null` |  | The open page (null is the root), for opening straight into one. Pair it with onPageChange; defaultPage for uncontrolled. |
| `defaultPage?` | `string \| null` | `""` | The page it opens on while uncontrolled (a page id; empty is the root, an unknown id is ignored). It returns there every time it reopens. The demo has the tema and limba pages. |
| `onPageChange?` | `(page: string \| null) => void` |  | Fires when a page is pushed or popped. |
| `query?` | `string` |  | Controlled search text; defaultQuery and onQueryChange complete the pair. |
| `defaultQuery?` | `string` | `""` | Search text it opens with while uncontrolled; the input is back to it every time it reopens. |
| `onQueryChange?` | `(query: string) => void` |  | Fires on every keystroke in the input (and when a page push or pop clears it). |
| `shortcut?` | `string` | `"mod+k"` | Toggles the menu from anywhere (⌘K on Apple devices, Ctrl+K elsewhere). "" binds nothing. The showcase binds ⌘K itself, so the demo uses ⌘J while this is mod+k. |
| `placeholder?` | `string` | `"Type a command or search…"` | The input placeholder on the root page; a page can set its own. |
| `emptyText?` | `string` | `"No results"` | Shown when nothing matches. |
| `loadingText?` | `string` | `"Searching…"` | Shown while loadItems runs and nothing else is listed yet; also the spinner's accessible name. |
| `recent?` | `boolean` | `true` | A Recent group of the last picked items (by id) while the query is empty on the root page. Page pushers are not recorded. |
| `recentKey?` | `string` | `"command-menu:recent"` | Local storage key for the recents (use-persisted-state). "" keeps them in memory for the session. |
| `maxRecent?` | `number` | `5` | How many recent items to show. |
| `footer?` | `boolean` | `true` | The key hints bar (↑↓ navigate, ↵ open, esc close) and the result count. Fine pointers only; never on the phone sheet. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Width 480 / 640 / 800px (the sheet from 768px with mode sheet: 420 / 560 / 720px). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"lg"` | Corner radius of the panel (0 / 12 / 16 / 24px) and, scaled down, of its rows and the phone search field. full keeps the 24px panel with pill rows. |
| `highlightMatches?` | `boolean` | `true` | Matched letters in bold. Matching ignores case and diacritics, so “cafe” highlights “Café”. |
| `globalShortcuts?` | `boolean` | `false` | Also binds every item's shortcut on the page while mounted, even closed (a bare key only outside text fields). Off: item shortcuts work only while the menu is open. |
| `loop?` | `boolean` | `true` | Arrow keys wrap from the last item to the first. |
| `responsive?` | `boolean` | `true` | A full-height bottom sheet below 768px, with the keyboard raised at once. |
| `mode?` | `"auto" \| "dialog" \| "sheet"` | `"auto"` | auto: the dialog from 768px and the sheet below (with responsive). dialog or sheet: that at every width. |
| `inline?` | `boolean` | `false` | Renders the palette in place instead of a dialog (a help page, a sidebar search). open and the shortcut are ignored; Escape clears. |
| `filter?` | `(item: CommandItem, query: string) => (number \| null)` |  | Custom ranking: higher first, null hides. Default: text-match over the label, keywords, description and the shortcut, best first inside each group. |
| `onSelect?` | `(item: CommandItem) => void` |  | After every pick (not page pushes), once the item's own onSelect settled. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | Router link for href items; defaults to the LinkProvider's. Clicks go through the link (cmd-click opens a new tab), Enter through useNavigate. |
| `container?` | `HTMLElement \| null` |  | Portal target, default document.body. |
| `labels?` | `{ title?; navigate?; select?; close?; back?; clear?; cancel?; recent?; error?; retry?; results?: (n) => string }` |  | Every other visible or spoken string: the hidden dialog title ("Command menu"), the footer hints (clear is the esc hint of an inline palette), the sheet's Cancel, the Recent heading, the error row and the announced result count. |
| `classNames?` | `Partial<Record<"overlay" \| "content" \| "header" \| "breadcrumb" \| "input" \| "spinner" \| "esc" \| "list" \| "group" \| "groupHeading" \| "item" \| "icon" \| "label" \| "description" \| "badge" \| "shortcut" \| "empty" \| "loading" \| "error" \| "footer", string>>` |  | Extra classes for the inner parts. |

## Parts

### CommandMenuTrigger (component)

A field-looking button (“Search or jump to…  ⌘K”) to pass as the menu's trigger. It shows the enclosing menu's shortcut, folds to an icon button below 768px, and also opens a controlled menu.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder?` | `string` | `"Search or jump to…"` | The text inside the field. |
| `shortcut?` | `string` |  | The hint at the end. Defaults to the enclosing menu's shortcut (mod+k); "" hides it. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Height 32 / 40 / 48px; md grows to 44px on touch screens. |
| `variant?` | `"soft" \| "outline" \| "ghost"` | `"soft"` | A tinted field, a bordered field or a bare one for toolbars. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape, scaled with the size like Button. |
| `icon?` | `ReactNode` |  | Replaces the search icon. |
| `responsive?` | `boolean` | `true` | Folds to a square icon button below 768px. |
| `classNames?` | `Partial<Record<"icon" \| "label" \| "shortcut", string>>` |  | Extra classes for the inner parts. |

### commandItemsFromNav (function)

Turns the app's navigation (NavItem[]) into command items for a “Go to” group, keeping href, icon, badge, keywords and shortcut. Nested items read “Invoices › Overdue”, so typing “invoices overdue” finds them.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `readonly NavItem[]` |  | The navigation tree, the same one the sidebar renders. |
| `options.nested?` | `boolean` | `true` | Include child items under their parent's label. |
| `options.separator?` | `string` | `" › "` | Placed between a parent and a child label. |
| `returns?` | `CommandItem[]` |  | Items ready for a group's items array. |

### commandItemText (function)

The searchable text of an item: its label when it is a string, else its textValue, else its id. Useful to filter or sort items your own way.

### CommandItem (type)

One row: { id, label, icon?, description?, badge?, keywords?, textValue?, shortcut?, href?, external?, page?, onSelect?, tone?, disabled?, closeOnSelect? }.

### CommandGroup (type)

A titled list of items: { id?, label?, items }.

### CommandPage (type)

A nested page an item opens: { id, title, placeholder?, groups?, loadItems? }.

### CommandMenuLabels (type)

Every built-in string (title, footer hints, Cancel, Recent, errors, result count) for translation.

## Examples

- **Nested pages**: Items that open pages, two levels deep, with breadcrumb chips.
- **Async search**: Server results with loading, empty and error + retry states.
- **Recents**: The last picked items come first when you reopen it.
- **Sizes**: sm, md and lg widths.
- **Phone**: The full-height sheet with the keyboard raised at once.
- **Global shortcuts**: Item shortcuts that work while the menu is closed.
- **People**: Avatars as icons, with descriptions.

## See also

- [Global Search](https://wingo-ui.com/components/global-search) ([Markdown](https://wingo-ui.com/components/global-search.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Shortcuts Dialog](https://wingo-ui.com/components/shortcuts-dialog) ([Markdown](https://wingo-ui.com/components/shortcuts-dialog.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))

---

Canonical: https://wingo-ui.com/components/command-menu
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
