# Command Menu

> Search everything from anywhere: ⌘K opens it over the page, results come grouped, and the arrows and Enter pick one.

- Name: `command-menu`
- Status: beta
- Categories: navigation
- File: the install writes `command-menu.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`. The install adds them.
- Other registry items: `@txbrendan/action-keys`. The install adds them.
- Gallery: https://txbrendan.dev/components/command-menu
- Registry item: https://txbrendan.dev/r/command-menu.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @txbrendan/command-menu
   ```

## Use it for

- One per app, to jump to people, teams, events, pages, and actions from anywhere.
- Keep open in state. Set it in onOpen, which ⌘K (Ctrl+K) calls, and clear it in onClose.
- Put a Search button on screen that opens it too. Teach ⌘K in that button's tooltip, never in the layout.
- Give each item a group, like "People" or "Actions". Groups show in the order they first appear, five results each.
- Mark a few items recent. They show before anything is typed.
- Use inline to show it open in a card or a docs page.

## Avoid it for

- Picking a value for a form. Use Combobox, which keeps the choice in the field.
- A short list of actions on one thing. Use Menu.
- Two command menus on one page with shortcut on. Only one should listen for ⌘K.

## Props

- `items`: `CommandItem[]`. The results: { id, person?, avatar?, label?, icon?, group?, meta?, keywords?, recent?, onSelect? }. A person has a name and an avatar element. Anything else has a label and an icon element.
- `open`: `boolean`. Default `false`. Whether it is open over the page. Not used with inline.
- `onOpen`: `() => void`. ⌘K (Ctrl+K) was pressed while it was closed.
- `onClose`: `() => void`. Esc, a press outside, ⌘K, or a choice closed it.
- `onSelect`: `(item: CommandItem) => void`. Runs after a result is chosen. It closes first, then the item's own onSelect runs, then this.
- `placeholder`: `string`. Default `"Search people, teams, and pages"`. The field's placeholder.
- `label`: `string`. Default `"Search"`. Names the dialog and the field for screen readers.
- `shortcut`: `boolean`. Default `true`. Listen for ⌘K on the page. A rich text editor keeps ⌘K for its links.
- `inline`: `boolean`. Default `false`. Open in the page with no scrim. Choosing a result or Esc clears the words.
- `effect`: `"rise" | "grow"`. Default `"rise"`. How the menu and its backdrop open and close. rise fades in and drops into place. grow fades in and grows in place, and closes the same way. Left out, it follows an effect-\<name> class on \<body>, where popups open, and rises with none. Not used with inline.
- `className`: `string`. Extra classes on the menu. Later classes win.

## Accessibility

- It is a modal dialog named by label. Focus goes to the field, Tab stays inside even when pressed fast, and the page behind is inert.
- When it closes, focus goes back to what opened it.
- The field is a combobox. The arrows move the highlight and Enter chooses. Focus stays in the field.
- Esc clears typed words first. The next Esc closes it.
- Results are grouped under their group's name, which screen readers hear.
- Avatars and icons are hidden from screen readers, since the name says who or what it is.

## Examples

### Open it with ⌘K or a button

```tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

const ITEMS = [
  { id: "sam", person: "Sam Carter", group: "People", meta: "Design lead, Mobile app", recent: true },
  { id: "design", label: "Design", group: "Teams" },
  { id: "fees", label: "Fees", group: "Pages" },
];

export function Search() {
  const [open, setOpen] = useState(false);
  const router = useRouter();
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>Search</button>
      <CommandMenu
        items={ITEMS}
        open={open}
        onOpen={() => setOpen(true)}
        onClose={() => setOpen(false)}
        onSelect={(item) => router.push(`/${item.id}`)}
      />
    </>
  );
}
```

### Open in the page

```tsx
<CommandMenu
  inline
  items={[
    { id: "new", label: "New event", group: "Actions", recent: true },
    { id: "fees", label: "Fees", group: "Pages" },
  ]}
/>
```

### Grows in place

```tsx
"use client";

import { useState } from "react";

const ITEMS = [
  { id: "design", label: "Design", group: "Teams" },
  { id: "fees", label: "Fees", group: "Pages" },
];

export function Search() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>Search</button>
      <CommandMenu items={ITEMS} open={open} onOpen={() => setOpen(true)} onClose={() => setOpen(false)} effect="grow" />
    </>
  );
}
```
