React Command Palette: Cmd+K, Nested Pages and Mobile
A React command palette is a search box in a dialog that lets people jump to a page, run an action or find a client without reaching for the mouse. With cmdk the first version is short. The details take longer: the hint has to read ⌘K on a Mac and Ctrl+K on Windows, "Theme…" should open a second page that Backspace closes, server results should arrive without flicker, recent picks should come first, and on a phone the whole thing should become a full-height sheet with the keyboard up. A basic cmd k search React example, shadcn's included, ends at the filtered list. This guide covers the rest with the Command Menu from Wingo UI, the library we build, so we are not neutral; outside facts link to their sources.
What does a React command menu need?
It needs a device-aware shortcut, forgiving ranking, nested pages, async results, recents and a phone layout. Our checklist for a React command menu:
- One shortcut, two notations.
mod+kbinds ⌘K on Apple devices and Ctrl+K elsewhere, and the hint shows the right one. - Groups and ranking. Matching ignores case and diacritics, so "cafe" finds "Café", and the matched letters turn bold.
- Nested pages. An item with a chevron opens a page; Backspace on an empty input or Escape goes back.
- Async results under the static ones, debounced, the previous request aborted.
- Recents by item id while the query is empty.
- Phones: a full-height sheet, 48px rows, a Cancel button, no key hints.
$ npx wingo-ui@latest add command-menuHow do I bind Cmd+K on a Mac and Ctrl+K on Windows?
Write the shortcut once as mod+k, resolve mod to Meta on Apple devices and Control elsewhere, compare the physical key when the layout types another character, and render the label for the device. The shadcn command palette example (opens in a new tab) toggles on this check and prints a fixed ⌘J hint:
Three things are off. Ctrl+J also fires on a Mac. Windows users see a ⌘ key they do not have. And event.key is the character the layout types, which on a Cyrillic layout can be a Cyrillic letter; event.code (opens in a new tab) names the physical key and "isn't altered by keyboard layout". The free useShortcut hook handles all three, and the free Kbd prints the label:
- The label renders as Ctrl+K on the server and switches to ⌘K after hydration, because the server cannot know the platform. Same trap as an SSR-safe useIsMobile hook.
aria-keyshortcuts(opens in a new tab) announces the shortcut; MDN notes it "has no effect on the functionality of the page", so the listener still does the work.- Bare keys such as
/stay quiet while focus is in a text field. Combos with a modifier fire anywhere.
Ctrl+K is taken: as of October 2026, Chrome's shortcut list (opens in a new tab) uses Ctrl+K on Windows and Linux to "Search from anywhere on the page". The listener calls preventDefault(), so the palette wins while the page has focus. MDN advises (opens in a new tab) against taking a browser shortcut "unless they are used for the same thing", and a search palette is close to that.
One cmdk React detail bites on Windows: the vimBindings (opens in a new tab) prop defaults to true, so Ctrl+J and Ctrl+K inside the list move the highlight and call preventDefault(). A listener that skips handled events, like useShortcut, then ignores the second Ctrl+K and the palette stays open. Pass vimBindings={false} if you want a toggle. The Command Menu keeps cmdk's default as of October 2026, so on Windows and Linux a second Ctrl+K moves the highlight up and Escape is the key that closes it.
$ npx wingo-ui@latest add use-shortcutHow do nested pages work in a command palette?
Pages are a stack. An item with page: "theme" pushes that page and clears the query; Backspace on an empty input or Escape pops it; Escape on the first page closes the menu. Going back restores the query you had and highlights the row that opened the page, which cmdk's own pages pattern (opens in a new tab) leaves to you. A page brings its own groups, or a loadItems that also runs for the empty query (the projects page in the full example below).
How do I load async results without flicker?
Debounce the query, abort the request it replaces, and render server results under the local ones instead of swapping the list. The Command Menu calls loadItems(query, { signal, page }) 200ms after the last keystroke (debounce), aborts the previous call through signal, shows a spinner in the input, keeps the static matches on screen, and turns a rejected promise into a "Could not load results" row with Try again. Pass signal to fetch so a stale request stops.
On raw cmdk, set shouldFilter={false} when you filter yourself or the server already did; otherwise cmdk filters and sorts the server's answer again. The Command Menu sets it too and ranks with its own text matcher. cmdk's README also says it has no virtualization, with "Good performance up to 2,000-3,000 items", so large data sets belong on the server.
How should recents work in a command palette?
Store item ids, never labels, and only for things the person ran. Picks go to local storage under recentKey ("command-menu:recent"), and the last maxRecent (5) show in a Recent group while the query is empty. Page openers are not recorded. Server results stay in recents for the current session only, since a reload cannot rebuild them from the static groups. Prefix server ids (client:42) so they never collide with static ones: when two items share an id, only the first one renders, and development builds warn about duplicates inside groups and pages.
What should a command palette do on a phone?
Open a full-height sheet with the keyboard already up. A phone has no ⌘ key, so the palette needs a visible trigger: CommandMenuTrigger reads "Search or jump to… ⌘K" on desktop and folds into an icon button below 768px. Below 768px the menu opens as a bottom sheet: an iOS-style search field, 48px rows, a Cancel button, swipe down to close, and no shortcut hints or footer on touch screens.
On iOS, a focus() call that does not come from the tap focuses the input and leaves the keyboard down. That is deliberate: in WebKit bug 195884 (opens in a new tab) an Apple engineer wrote in 2019 that Apple does "not want programmatic focus to bring up the keyboard", and the bug is still open in October 2026. The sheet's input does not exist yet when the tap lands, so the trigger focuses a hidden input inside the tap handler and hands focus to the real one once the sheet mounts.
Keep the input at 16px. Safari on iOS zooms into a focused input whose text is 15px or smaller (CSS-Tricks (opens in a new tab)), and shadcn's CommandInput (opens in a new tab) uses text-sm (14px) as of October 2026.
$ npx wingo-ui@latest add command-menuIs a cmdk command palette accessible?
Yes for the core widget. cmdk renders the input as a combobox that controls a listbox and sets aria-activedescendant to the highlighted option, so screen readers follow the highlighted row while focus stays in the input, and its README says labeling and ARIA attributes were "tested with Voice Over and Chrome DevTools". The Command Menu adds a hidden dialog title, aria-keyshortcuts on rows with a shortcut, and a polite live region that announces the result count after typing pauses. For translation, the input text comes from placeholder, emptyText and loadingText, and every other visible or spoken string from labels.
Should you use the shadcn command palette, cmdk or a ready-made menu?
Use shadcn's Command when you want free MIT code and will write the shortcut, pages, async loading and phone layout yourself. Use cmdk directly for an unstyled base. Use the Command Menu when you want those as props. As of October 2026, from the cmdk README (opens in a new tab) and the shadcn/ui Command docs (opens in a new tab):
If people mostly search records (clients, invoices) with scopes and "See all" links, a header field fits better: Global Search binds /, groups results by type and opens full screen on phones. It works next to a palette: / to search, ⌘K for commands.
How do I add a React command palette to a Next.js app?
Install it, hand the library your router once, and mount the menu in your app shell. LinkProvider lets link rows render through next/link (so Cmd+click opens a new tab) and lets Enter navigate with router.push:
The menu, with navigation, actions, two pages and server search:
commandItemsFromNav turns your sidebar items into "Go to" rows, and nested ones read "Invoices › Overdue". An onSelect that returns a promise keeps the menu open with a spinner on that row until it settles. Render <AppCommandMenu /> in your app header, inside Providers and the next-themes ThemeProvider, so link rows reach the router and the Theme page can switch themes.
The Command Menu and Global Search are part of Wingo UI Pro, $8 a month, $80 a year or $150 once as of October 2026 (pricing). Kbd and useShortcut are free: npx wingo-ui@latest add kbd use-shortcut. More in the component guides, starting with the React form components guide.
FAQ
How do I open a command palette with Cmd+K in React?
Listen for keydown on window and toggle the palette when Meta and K are pressed on Apple devices, or Control and K elsewhere, then call preventDefault so the browser's own Ctrl+K search does not run. Wingo UI's free useShortcut hook, called as useShortcut("mod+k", toggle), does the platform check and also matches the physical K key on non-Latin layouts.
Does cmdk filter async results?
Yes, by default cmdk filters and sorts every rendered item, server results included. Set shouldFilter={false} on Command when you filter yourself or the server already returns matches, and render Command.Loading while the request runs.
How many items can cmdk handle?
The cmdk README says it has no virtualization and performs well up to 2,000 to 3,000 items. Past that, search on the server and return the top matches.
Why does a second Ctrl+K not close my cmdk palette on Windows?
cmdk's vimBindings prop defaults to true, so Ctrl+J and Ctrl+K inside the palette move the highlight and call preventDefault. A shortcut listener that skips handled events then ignores the press; pass vimBindings={false} if you want Ctrl+K to toggle.
How should a command palette work on mobile?
Give it a visible search button, since phones have no Cmd key, and open it as a full-height sheet with the input focused inside the tap so iOS raises the keyboard. Use 16px input text so Safari does not zoom, and rows at least 44px tall.
Is the Wingo UI command menu free?
No. The Command Menu and Global Search are part of Wingo UI Pro. The Kbd component and the useShortcut hook are free and install with npx wingo-ui@latest add kbd use-shortcut.
- Command Palette
- Keyboard Shortcuts
- cmdk
- Mobile UI
- Accessibility