# Command Menu

A dialog with a search field that opens a page or runs a command.

```tsx
import * as React from 'react'
import { Button, CommandMenu, type CommandMenuGroup } from 'ferry-ui'
import { CreditCard, FolderKanban, LayoutDashboard, Plus, Search, UserPlus, Users } from 'lucide-react'

export default function CommandMenuHero() {
  const [open, setOpen] = React.useState(false)
  const [last, setLast] = React.useState('none')

  // Navigation first, actions last. In a real app, a navigation item has an `href`.
  const groups: CommandMenuGroup[] = [
    {
      id: 'pages',
      label: 'Go to',
      items: [
        { id: 'overview', label: 'Overview', icon: <LayoutDashboard />, onSelect: () => setLast('Go to Overview') },
        { id: 'projects', label: 'Projects', icon: <FolderKanban />, onSelect: () => setLast('Go to Projects') },
        { id: 'members', label: 'Members', icon: <Users />, onSelect: () => setLast('Go to Members') },
        { id: 'billing', label: 'Billing', icon: <CreditCard />, onSelect: () => setLast('Go to Billing') },
      ],
    },
    {
      id: 'actions',
      label: 'Actions',
      items: [
        { id: 'new-project', label: 'New project', icon: <Plus />, onSelect: () => setLast('New project') },
        { id: 'invite', label: 'Invite member', icon: <UserPlus />, onSelect: () => setLast('Invite member') },
      ],
    },
  ]

  return (
    <div className="flex flex-col items-center gap-3">
      <Button icon={<Search />} onClick={() => setOpen(true)}>
        Open the command menu
      </Button>
      <p className="text-[13px] text-foreground-light">Last command: {last}</p>
      <CommandMenu open={open} onOpenChange={setOpen} groups={groups} />
    </div>
  )
}
```

The search of this site is a command menu. Press <Kbd>⌘ K</Kbd> or <Kbd>Ctrl K</Kbd> to open it.

## Usage guidelines

- **One menu for each app.** Mount it one time, near the root of the app.
- **Navigation first.** Put the pages in the first groups and the actions in the last groups. With an empty search field, <Kbd>Enter</Kbd> then opens a page.
- **The same items as the rail.** A `NavGroup[]` is a valid value for `groups`. See [`NavItem`](/docs/components/icon-rail#navitem).
- **Not a field of a form.** For a list with a search field in a form, use [Command](/docs/components/command) in a [Popover](/docs/components/popover).

## Anatomy

The menu has one part. Its `groups` prop gives the commands.

```tsx title="Anatomy"

<CommandMenu open={open} onOpenChange={setOpen} groups={groups} />
```

## Examples

### Links and actions

An item with `href` opens a page through the link component of the app. An item with `onSelect` runs an action. The menu then closes.

```tsx
const groups: CommandMenuGroup[] = [
  {
    id: 'pages',
    label: 'Go to',
    items: [{ id: 'invoices', label: 'Invoices', icon: <Receipt />, href: '/invoices' }],
  },
  {
    id: 'actions',
    label: 'Actions',
    items: [{ id: 'new-invoice', label: 'New invoice', icon: <Plus />, onSelect: openNewInvoice }],
  },
]
```

### Shortcut

Set `shortcut`. The menu then opens and closes on <Kbd>⌘ K</Kbd> or <Kbd>Ctrl K</Kbd>, with no state in your code. A letter sets a different key: `shortcut="j"`.

```tsx
<CommandMenu groups={groups} shortcut />
```

<Callout tone="warning" title="Bind the shortcut in one place">
  Use only one of `shortcut`, [`useCommandShortcut`](/docs/utilities/use-command-shortcut) and the `onCommandShortcut` prop of [App Shell](/docs/components/app-shell). With two of them, one key press opens the menu and closes it immediately.
</Callout>

### Item options

An item accepts more fields than a `NavItem`. For example, `keepOpen` keeps the menu open after the command runs. The API reference lists these fields.

```tsx
import * as React from 'react'
import { Badge, Button, CommandMenu, useModKey, type CommandMenuGroup } from 'ferry-ui'
import { CreditCard, FolderKanban, KeyRound, Plus, Rows3 } from 'lucide-react'

export default function CommandMenuItemOptions() {
  const [open, setOpen] = React.useState(false)
  const [compact, setCompact] = React.useState(false)
  const mod = useModKey()

  const groups: CommandMenuGroup[] = [
    {
      id: 'pages',
      label: 'Go to',
      items: [
        // `keywords`: the query "payment" finds this item.
        { id: 'billing', label: 'Billing', icon: <CreditCard />, hint: 'Plan and invoices', keywords: ['payment'] },
        { id: 'projects', label: 'Projects', icon: <FolderKanban />, badge: <Badge variant="info">New</Badge> },
      ],
    },
    {
      id: 'actions',
      label: 'Actions',
      items: [
        // `shortcut` shows the keys. Your code binds them.
        { id: 'new-project', label: 'New project', icon: <Plus />, shortcut: `${mod} N` },
        {
          id: 'compact',
          label: 'Compact rows',
          icon: <Rows3 />,
          hint: compact ? 'On' : 'Off',
          keepOpen: true,
          onSelect: () => setCompact((value) => !value),
        },
        { id: 'api-key', label: 'New API key', icon: <KeyRound />, hint: 'Admins only', disabled: true },
      ],
    },
  ]

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open the command menu</Button>
      <CommandMenu open={open} onOpenChange={setOpen} groups={groups} />
    </>
  )
}
```

### Results from a server

Set `shouldFilter` to `false`. The menu then shows each item that you pass. Get the results in `onSearchChange`. While the request is in progress, set `loading`.

```tsx
import * as React from 'react'
import { Button, CommandMenu, type CommandMenuGroup } from 'ferry-ui'
import { FileText } from 'lucide-react'

const INVOICES = [
  { id: 'INV-2041', customer: 'Northwind Trading', amount: '$4,200.00' },
  { id: 'INV-2042', customer: 'Acme', amount: '$860.00' },
  { id: 'INV-2043', customer: 'Globex Logistics', amount: '$12,940.50' },
  { id: 'INV-2044', customer: 'Umbrella Health', amount: '$7,020.00' },
]

export default function CommandMenuRemote() {
  const [open, setOpen] = React.useState(false)
  const [results, setResults] = React.useState(INVOICES)
  const [loading, setLoading] = React.useState(false)
  const timer = React.useRef<number | undefined>(undefined)

  // A timer plays the role of the server. The menu calls this function with "" each time it opens.
  const search = (query: string) => {
    window.clearTimeout(timer.current)
    setLoading(true)
    timer.current = window.setTimeout(() => {
      const text = query.trim().toLowerCase()
      setResults(INVOICES.filter((invoice) => `${invoice.id} ${invoice.customer}`.toLowerCase().includes(text)))
      setLoading(false)
    }, 500)
  }

  const groups: CommandMenuGroup[] = [
    {
      id: 'invoices',
      label: 'Invoices',
      items: results.map((invoice) => ({
        id: invoice.id,
        label: `${invoice.id} · ${invoice.customer}`,
        icon: <FileText />,
        hint: invoice.amount,
      })),
    },
  ]

  return (
    <>
      <Button onClick={() => setOpen(true)}>Search invoices</Button>
      <CommandMenu
        open={open}
        onOpenChange={setOpen}
        groups={groups}
        shouldFilter={false}
        onSearchChange={search}
        loading={loading}
        loadingMessage="Search in progress…"
        emptyMessage="No invoice matches."
        placeholder="Search invoices by number or customer…"
      />
    </>
  )
}
```

### Custom rows

The children show after the groups. Use `CommandGroup` and `CommandItem` for a row with your own content. A custom row does not close the menu. Close the menu in the `onSelect` of the row.

```tsx
import * as React from 'react'
import {
  Avatar,
  AvatarFallback,
  Button,
  CommandGroup,
  CommandItem,
  CommandMenu,
  CommandSeparator,
  toast,
  type CommandMenuGroup,
} from 'ferry-ui'
import { FolderKanban } from 'lucide-react'

// In a real app, this item has an `href` or an `onSelect`.
const GROUPS: CommandMenuGroup[] = [
  { id: 'pages', label: 'Go to', items: [{ id: 'projects', label: 'Projects', icon: <FolderKanban /> }] },
]

const MEMBERS = [
  { name: 'Maya Chen', initials: 'MC', email: 'maya@example.com' },
  { name: 'Sam Lee', initials: 'SL', email: 'sam@example.com' },
]

export default function CommandMenuCustomRows() {
  const [open, setOpen] = React.useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open the command menu</Button>
      <CommandMenu open={open} onOpenChange={setOpen} groups={GROUPS}>
        <CommandSeparator />
        <CommandGroup heading="Members">
          {MEMBERS.map((member) => (
            <CommandItem
              key={member.email}
              value={`${member.name} ${member.email}`}
              onSelect={() => {
                // A custom row does not close the menu. Close it in your code.
                setOpen(false)
                toast(`${member.name} selected`)
              }}
            >
              <Avatar size="sm">
                <AvatarFallback>{member.initials}</AvatarFallback>
              </Avatar>
              <span className="flex min-w-0 flex-col">
                <span className="truncate text-foreground">{member.name}</span>
                <span className="truncate text-[12px] text-foreground-lighter">{member.email}</span>
              </span>
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandMenu>
    </>
  )
}
```

## Accessibility

- The focus moves to the search field when the menu opens. <Kbd>↑</Kbd> and <Kbd>↓</Kbd> move in the list. <Kbd>Enter</Kbd> runs the command. <Kbd>Esc</Kbd> closes the menu.
- `title` and `description` name the dialog for screen readers. They do not show.
- `closeLabel` and `externalLabel` set two more texts for screen readers. Use them to translate these texts.

## API reference

`CommandMenu` accepts only the props of this table.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `groups` (required) | `CommandMenuGroup[]` |  | Sections of commands, separated by hairlines while idle. `label` renders as the mono group heading. Put navigation first and side-effect actions last, so that opening the menu and pressing Enter runs something harmless; while searching, groups are ranked by best match. |
| `open` | `boolean` |  | Controlled open state. Pair with `onOpenChange` (and `useCommandShortcut` to toggle it). |
| `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. Defaults to `false`. |
| `shortcut` | `string \| boolean` | `false` | Lets the menu bind its own global shortcut and toggle itself: `true` for ⌘K (Apple) / Ctrl+K (elsewhere), or another letter (`"j"` for ⌘J). Works controlled (it calls `onOpenChange`) and uncontrolled. Leave it at `false` (the default) when you toggle the menu yourself with `useCommandShortcut` or `AppShell`'s `onCommandShortcut`, or each press would toggle twice. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called when the menu opens or closes (Escape, outside click, after running a command). |
| `placeholder` | `string` | `Type a command or search…` | Placeholder of the search field. Defaults to `"Type a command or search…"`. |
| `title` | `string` | `Command menu` | Accessible dialog title (visually hidden). Defaults to `"Command menu"`. |
| `description` | `string` | `Search for a page or run a command` | Accessible dialog description (visually hidden). Defaults to `"Search for a page or run a command"`. |
| `emptyMessage` | `ReactNode` | `No results found.` | Shown when no command matches the query. Defaults to `"No results found."`. |
| `loading` | `boolean` | `false` | Shows a loading row above the groups and hides the empty message, e.g. while remote results for the current query are fetched. |
| `loadingMessage` | `ReactNode` | `Loading…` | Text of the loading row. Defaults to `"Loading…"`. |
| `onSearchChange` | `((query: string) => void)` |  | Called when the query changes (and with `""` each time the menu opens, since every opening starts from an empty query). Use it to fetch remote results. |
| `shouldFilter` | `boolean` | `true` | Set `false` when you filter yourself (e.g. server-side search): every item you pass is shown as-is. Defaults to `true` (fuzzy matching on `value` / `label` and `keywords`). |
| `onItemSelect` | `((item: CommandMenuItem) => void)` |  | Called after any command runs (after its own `onSelect`), e.g. for analytics. |
| `linkComponent` | `LinkComponent` |  | Router-aware link used for `href` items. Defaults to the nearest `LinkProvider` (a plain `<a>`). Not used for `external` items (they render a plain `<a target="_blank">`). |
| `closeLabel` | `string` |  | Accessible name of the close (X) button. Defaults to `"Close"`; pass a translation in localized apps. |
| `externalLabel` | `string` | `(opens in a new tab)` | Screen-reader text appended to the name of `external` items. Defaults to `"(opens in a new tab)"`; pass a translation in localized apps. |
| `children` | `ReactNode` |  | Extra content appended inside the list, after the groups: `CommandGroup` / `CommandItem` / `CommandSeparator` from the Command primitive, for rows `groups` cannot express (custom row markup). They are filtered like the other rows, but their `onSelect` does not close the menu: call `onOpenChange(false)` yourself. |
| `className` | `string` |  | Extra classes for the dialog panel (e.g. to change its width). |

### CommandMenuItem

A `CommandMenuItem` has the fields of a `NavItem` and these fields. The menu ignores `active`.

| Field | Type | Role |
| --- | --- | --- |
| `hint` | `ReactNode` | Secondary text on the right. |
| `shortcut` | `ReactNode` | A key hint on the right. The menu does not bind the key. |
| `keywords` | `string[]` | More words that the search matches. |
| `value` | `string` | The text that the search matches. The default is `label`. It must be unique. |
| `keepOpen` | `boolean` | Keeps the menu open after the command runs. |

### CommandMenuGroup

A `CommandMenuGroup` is a `NavGroup` with `CommandMenuItem` items. The menu does not show a group with no item.
