How to Add a Command Palette to an AI-Built App

A practical guide to building a command palette for an AI-built app: when it clears the power-user threshold, the fuzzy-search command registry pattern, and how it differs from full-text search.

Steve Jefferson
Steve Jefferson
Developer Advocate
25 August 20261 min read

A command palette is a keyboard-driven menu, opened with Cmd+K or Ctrl+K, that lets a user jump to any screen or trigger any action by typing a few letters instead of hunting through menus and buttons. Building one for an AI-built app comes down to four pieces: a flat registry of every route and action, a global keyboard shortcut that opens a modal, a fuzzy filter that ranks the registry as the user types, and arrow-key navigation with Enter to run the selected item. It is a distinct feature from full-text search, and it is only worth the build effort past a specific density of commands and repeat users, not for every app.

It is easy to conflate the two because both start with a text box and a ranked list of results. They are not doing the same job. How to Add Search to an AI-Built App covers full-text content search: a user types a query and the system returns matching rows from your actual data, invoices, customers, support tickets, articles. That is a database and ranking problem, built against your content tables and typically backed by Postgres full-text search or a dedicated search engine.

A command palette searches something much smaller and static: a hand-maintained list of routes and actions, not your data. Typing "inv" in a command palette should surface "Create Invoice" and "Go to Invoices," not the rows in your invoices table. It is a navigation and action launcher, and it does not touch your database at query time at all. If your app already has content search and you are wondering whether a command palette adds anything on top of it, the answer is yes for speed of navigation and action execution, but no if you expect it to also search your content, because that is the other feature, built a different way.

When it is actually worth building

Not every app needs one. A command palette earns its build and maintenance cost only when two conditions hold at the same time: the app has enough distinct destinations and actions that browsing them is genuinely slow, and enough of the user base returns often enough to learn and reuse a keyboard shortcut. General UX guidance on the pattern reaches the same conclusion, recommending it for products with a large command surface used by frequent, efficiency-focused users rather than as a default addition.

Use a rough headcount test for the first condition. List every settings page, creation form, toggle, report, and standalone action in the app. Under about 15 to 20 items, skip the palette. Users can reach anything from the existing navigation in two clicks, and a searchable command list adds a feature with no real audience. Past that count, especially once items sit three or more menu levels deep, typing a few letters starts beating clicking through nested menus.

The second condition is about usage pattern, not total users. If roughly a fifth or more of active users open the app several times a week and repeat the same handful of actions each session, that is a power-user density worth serving. An internal ops tool, an admin console, or a CRM your own team lives in daily clears that bar easily; a admin dashboard is a common home for exactly this kind of repeated action. A consumer app that most people open once a week to complete one task will not develop a population of keyboard-fluent users no matter how many commands get registered. Build it for dense, frequently used tools. Skip it for onboarding flows, marketing pages, and apps where the primary path is a handful of screens visited rarely.

The minimal real implementation: a fuzzy-search command registry

The pattern that works without pulling in a heavy dependency is a command registry: one flat list of command objects that a fuzzy filter searches and a keyboard handler drives. This is the fuzzy-search command registry pattern in full, in build order.

  1. Define the registry as a flat array, not scattered across components. Each entry needs an id, a label, a list of extra matching keywords, a section for grouping such as Navigation, Actions, or Settings, and a run function that executes when the entry is selected.

  2. Populate it once at app init from your route table and your list of top-level actions, so adding a new page or action later means adding one registry entry, not wiring a new piece of UI by hand.

  3. Attach a single global keyboard listener for the open shortcut, Cmd+K on Mac and Ctrl+K on Windows and Linux, checked against a modifier map so it does not collide with browser or OS shortcuts already bound to that combination.

  4. Filter the registry with a lightweight fuzzy scorer as the user types: check the label and keyword list, weight matches at the start of a word higher than matches in the middle, and rank consecutive character runs above scattered ones, so typing "cust inv" still surfaces "Create Customer Invoice."

  5. Render results grouped by section with arrow-key movement, Enter to run the highlighted item, and Escape to close. Cap visible results at around 8 to 10 so the list stays scannable instead of turning into a scroll.

  6. Log every invocation, the typed query and the command picked, and use two or three weeks of that data to reorder and prune the registry. The entries nobody ever selects are the ones cluttering the results, not the ones worth defending.

  7. Roll out static entries first, fixed routes like "Go to Settings" or "Go to Billing," before adding dynamic per-record actions like "Open this specific invoice." Static entries are lower risk and prove the keyboard handling works before data-dependent commands get added on top.

Teams that do not want to hand-roll the keyboard handling and filtering often reach for cmdk, an unstyled React component that implements the registry-and-filter pattern above and is used in Vercel's own command menu. It saves the keyboard-event and focus-trap plumbing, but the registry design itself is the same whether it is built by hand or imported.

A single registry entry needs only a handful of fields to work:

  • id: a stable string such as "nav-settings," used for logging and de-duplication

  • label: the text shown and matched against, such as "Go to Settings"

  • keywords: extra terms that should also match, such as "preferences" and "account"

  • section: a grouping label used to cluster results, such as "Navigation"

  • run: the function executed on selection, usually a route change or a state update

Keeping it fast and out of the way

A palette that is slow or hard to find is worse than no palette. A few implementation details separate the two outcomes.

  • Filter on the client against the in-memory registry, not a network request. A registry of a few hundred entries filters in well under a millisecond, and a round trip would only add latency to something that should feel instant.

  • Show a small visible hint, such as a "Cmd+K" badge near the search icon or in the header, since a hidden keyboard shortcut with no on-screen trace will not get discovered no matter how useful it is.

  • Keep a mouse-usable entry point too, a button that opens the same modal, for the share of users on a device without a physical keyboard or who never learn the shortcut.

  • Trap focus inside the open modal, support Tab and Shift+Tab, and label results for screen readers. A command palette is a dense keyboard interface, and building it so it is fully operable without a mouse overlaps directly with the work covered in How to Make an AI-Built App Accessible.

  • Test what happens on a phone. Either hide the palette's entry point below a breakpoint where there is no physical keyboard, or replace it with a plain search-style list, rather than shipping a keyboard-first interaction to a touch-only screen.

That accessibility overlap is worth taking seriously rather than treating as a checkbox; see How to Make an AI-Built App Accessible for the fuller keyboard-navigation and screen-reader requirements a palette needs to meet.

FAQ

Do I need a library, or can I build a command palette from scratch?

Either works. The registry-plus-fuzzy-filter pattern above is the whole implementation, and it is small enough to build directly. A library like cmdk mainly saves the work of writing keyboard-event handling and focus-trap logic by hand; the underlying design is the same either way.

A search bar searches your content, rows in your database such as customers or documents. A command palette searches a small, fixed list of routes and actions maintained by hand, and it does not touch your data at query time. The two can live in the same app and even open from a similar keyboard shortcut, but they answer different questions.

How many commands is too many for one palette?

There is no hard ceiling if the fuzzy filter and grouping are working, since the user never scrolls the full list, they type a few letters and see the top matches. The practical limit is maintenance: past a few hundred entries, keep sections tight and prune anything the usage log shows nobody picks.

Should a command palette be available to every user, or just admins?

Scope it to whichever roles actually have enough actions to search through. In a role-based app, an admin with access to every settings page and bulk action benefits far more than a viewer with three permitted actions, so it is reasonable to only expose the palette, or expose a shorter registry, to roles above a certain permission level.

Does adding a command palette slow down page load?

Not meaningfully if it is lazy-loaded, meaning the modal component and registry only load when the user first presses the open shortcut rather than on initial page render. The registry is just data, a few hundred small objects, and filtering it client-side is fast enough that the palette is never the bottleneck.

Related: this fits the same build pattern as the other feature guides under how to build an app with AI: scope one feature clearly, decide whether it clears the power-user-density threshold before writing any code, then implement it as a self-contained registry an AI coding agent can extend one entry at a time.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.