Command Menu
betaSearch everything from anywhere: ⌘K opens it over the page, results come grouped, and the arrows and Enter pick one.
Preview
over the page
Nothing chosen yet
inline: in a card or a docs page
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
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
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.
CommandItem[]
- open
Whether it is open over the page. Not used with inline.
boolean · default false
- onOpen
⌘K (Ctrl+K) was pressed while it was closed.
() => void
- onClose
Esc, a press outside, ⌘K, or a choice closed it.
() => void
- onSelect
Runs after a result is chosen. It closes first, then the item's own onSelect runs, then this.
(item: CommandItem) => void
- placeholder
The field's placeholder.
string · default "Search people, teams, and pages"
- label
Names the dialog and the field for screen readers.
string · default "Search"
- shortcut
Listen for ⌘K on the page. A rich text editor keeps ⌘K for its links.
boolean · default true
- inline
Open in the page with no scrim. Choosing a result or Esc clears the words.
boolean · default false
- effect
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.
"rise" | "grow" · default "rise"
- className
Extra classes on the menu. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| 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 | 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 | "Search people, teams, and pages" | The field's placeholder. |
| label | string | "Search" | Names the dialog and the field for screen readers. |
| shortcut | boolean | true | Listen for ⌘K on the page. A rich text editor keeps ⌘K for its links. |
| inline | boolean | false | Open in the page with no scrim. Choosing a result or Esc clears the words. |
| effect | "rise" | "grow" | "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
"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
<CommandMenu
inline
items={[
{ id: "new", label: "New event", group: "Actions", recent: true },
{ id: "fees", label: "Fees", group: "Pages" },
]}
/>Grows in place
"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" />
</>
);
}