Command Menu React component for Tailwind CSS v4
v1.0.1Updated in 1.6.0
A React command menu (⌘K palette) with groups, shortcuts, nested pages, async results and recents, and a full-height sheet on phones.
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.
Command Menu is a Pro React component in the React navigation components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add command-menu, together with Badge, Drawer, Kbd, Spinner, Motion presets and 8 more and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, cmdk, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add command-menuCommand Menu is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import CommandMenu from @/components/ui/command-menu.
"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
groupsCommandGroup[]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).
pagesCommandPage[]Pages pushed by items with page: { id, title, placeholder?, groups?, loadItems? }. A page's own groups and loader replace the root ones.
triggerReactNodeRendered 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.
debouncedefault200numberMilliseconds of quiet typing before loadItems runs.
defaultOpendefaultfalsebooleanStarts open. Use open and onOpenChange to control it.
openbooleanControlled open state; pair it with onOpenChange.
onOpenChange(open: boolean) => voidFires when the menu opens or closes.
pagestring | nullThe open page (null is the root), for opening straight into one. Pair it with onPageChange; defaultPage for uncontrolled.
defaultPagedefault""string | nullThe 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) => voidFires when a page is pushed or popped.
querystringControlled search text; defaultQuery and onQueryChange complete the pair.
defaultQuerydefault""stringSearch text it opens with while uncontrolled; the input is back to it every time it reopens.
onQueryChange(query: string) => voidFires on every keystroke in the input (and when a page push or pop clears it).
shortcutdefault"mod+k"stringToggles 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.
placeholderdefault"Type a command or search…"stringThe input placeholder on the root page; a page can set its own.
emptyTextdefault"No results"stringShown when nothing matches.
loadingTextdefault"Searching…"stringShown while loadItems runs and nothing else is listed yet; also the spinner's accessible name.
recentdefaulttruebooleanA Recent group of the last picked items (by id) while the query is empty on the root page. Page pushers are not recorded.
recentKeydefault"command-menu:recent"stringLocal storage key for the recents (use-persisted-state). "" keeps them in memory for the session.
maxRecentdefault5numberHow many recent items to show.
footerdefaulttruebooleanThe key hints bar (↑↓ navigate, ↵ open, esc close) and the result count. Fine pointers only; never on the phone sheet.
sizedefault"md""sm" | "md" | "lg"Width 480 / 640 / 800px (the sheet from 768px with mode sheet: 420 / 560 / 720px).
radiusdefault"lg""none" | "sm" | "md" | "lg" | "full"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.
highlightMatchesdefaulttruebooleanMatched letters in bold. Matching ignores case and diacritics, so “cafe” highlights “Café”.
globalShortcutsdefaultfalsebooleanAlso 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.
loopdefaulttruebooleanArrow keys wrap from the last item to the first.
responsivedefaulttruebooleanA full-height bottom sheet below 768px, with the keyboard raised at once.
modedefault"auto""auto" | "dialog" | "sheet"auto: the dialog from 768px and the sheet below (with responsive). dialog or sheet: that at every width.
inlinedefaultfalsebooleanRenders 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) => voidAfter every pick (not page pushes), once the item's own onSelect settled.
linkComponent(props: LinkComponentProps) => ReactNodeRouter link for href items; defaults to the LinkProvider's. Clicks go through the link (cmd-click opens a new tab), Enter through useNavigate.
containerHTMLElement | nullPortal 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.
classNamesPartial<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
componentA 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.
placeholderdefault"Search or jump to…"stringThe text inside the field.
shortcutstringThe hint at the end. Defaults to the enclosing menu's shortcut (mod+k); "" hides it.
sizedefault"md""sm" | "md" | "lg"Height 32 / 40 / 48px; md grows to 44px on touch screens.
variantdefault"soft""soft" | "outline" | "ghost"A tinted field, a bordered field or a bare one for toolbars.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape, scaled with the size like Button.
iconReactNodeReplaces the search icon.
responsivedefaulttruebooleanFolds to a square icon button below 768px.
classNamesPartial<Record<"icon" | "label" | "shortcut", string>>Extra classes for the inner parts.
commandItemText
functionThe 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
typeOne row: { id, label, icon?, description?, badge?, keywords?, textValue?, shortcut?, href?, external?, page?, onSelect?, tone?, disabled?, closeOnSelect? }.
CommandGroup
typeA titled list of items: { id?, label?, items }.
CommandPage
typeA nested page an item opens: { id, title, placeholder?, groups?, loadItems? }.
CommandMenuLabels
typeEvery 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.
Dependencies
Files
- components/ui/command-menu.tsx
FAQ
Is the React Command Menu component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Command Menu?
Run npx wingo-ui@latest add command-menu. It also adds the Wingo UI items it builds on (Badge, Drawer, Kbd, Spinner, Motion presets and 8 more) and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, cmdk, lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/command-menu.
How does Command Menu behave on phones?
A full-height bottom sheet below 768px, with the keyboard raised at once. The responsive prop controls this and is on by default.
See also
Used in
More React navigation components
Articles using this component
- React Command Palette: Cmd+K, Nested Pages and Mobile
Component guides9 min read