API Reference

Everything you pass to the provider and every hook, plus the command object. The framework-agnostic building blocks are on the Core API page, and the UI and router adapters on Adapters.

ImportContains
cmdk-engineThe core engine and all types
cmdk-engine/reactCommandEngineProvider and the hooks
cmdk-engine/adapters/cmdkCommandPalette and useCommandPaletteShortcut for cmdk
cmdk-engine/adapters/base-uiThe same two exports for Base UI
cmdk-engine/adapters/react-routerscanRoutes
cmdk-engine/adapters/sitemapsitemapToCommands
cmdk-engine/search/match-sortercreateMatchSorterSearch

CommandItem

A command. Register commands with useCommandRegister, or put them in children of another command. Only id and label are required.

FieldTypeDescription
idstringUnique id. It is also the frecency key, so keep it stable.
labelstringThe text shown in the palette. Searched with the highest weight.
descriptionstringSecondary text, also searched.
iconReactNodeA string, emoji or element shown before the label.
keywordsstring[]Extra search terms.
groupstringThe heading to list the command under. Matches a groups id.
prioritynumberHigher first: breaks score ties, and orders the empty-query list.
hrefstringWhere to go when selected. Handled by onNavigate, or window.location.
action(item) => void | Promise<void>Runs when selected. Wins over href.
disabledbooleanShown, but selecting it does nothing.
hiddenbooleanLeft out of the empty-query list, but still found by a query.
permissionsstring[]Permissions needed to see the command. See accessControl.
accessMode'any' | 'all'Overrides accessCheckMode for this command.
whenboolean | () => booleanA visibility gate. When it is false the command is removed: not searchable, not browsable.
shortcutstring[]Keys shown in the row, such as ["g", "h"]. Display only: nothing binds these keys.
metaRecord<string, unknown>Your own data. The engine passes it through.
scopestring[]Paths or tags where the command ranks higher. See context.
childrenCommandItem[]Makes the command a sub-menu. See Nested Commands.
parentIdstringDeprecated. Never set or read by the engine; kept so existing code compiles. Keep your own parent id in meta.

CommandEngineProvider

Initializes the engine and makes it available to every hook and adapter below it. It takes children and an optional config prop of type CommandEngineConfig. Every option is optional.

OptionTypeDefaultDescription
searchEngineSearchEnginebuilt-in fuzzy searchSee Search.
accessControlAccessControlProvidernoneDecides which commands the user may see, from each command's permissions. Build one with createSimpleAccessProvider([...]). This is a UI filter, not a security boundary: enforce permissions on the server.
accessCheckMode'any' | 'all''any'Whether a user needs any or all of a command's permissions.
synonymsSynonymMapnoneA dictionary such as { billing: ['money', 'payment'] }, used both ways. See Search.
frecencyFrecencyOptions & RecentCommandsConfig & { enabled?: boolean }onUsage-based ranking and the Recent group. Set enabled: false to turn it off. Options below.
groupsCommandGroup[]noneLabels and order for headings. See Nested Commands and Groups.
maxResultsnumber50The most results to return, across groups.
onSelect(item) => voidnoneRuns for every selected command instead of its action and instead of onNavigate. See the precedence below.
onNavigate(href, item) => voidwindow.location.href = hrefRuns for a selected command that has an href and no action. Pass your router here to navigate without a reload.
onSelectError New in 0.6(error: unknown, item: CommandItem) => voidnoneCalled when the handler a selection runs (onSelect, action or onNavigate) throws or returns a rejected promise. The palette still closes. Without it, a throw propagates and a rejection stays unhandled. Async source failures go to asyncErrors instead.
contextCommandContextnoneThe current location, { path?, tags? }, for scope boosting.
contextBoostWeightnumber0.2How much to boost commands in scope, from 0 to 1.
t(key, params?) => stringEnglishTranslates the built-in UI strings. See the i18n keys below.
localestringnoneDeprecated. Never read: nothing in cmdk-engine depends on the locale. Kept so existing configs compile. Localize UI strings with t.
searchHistorySearchHistoryConfigoffRecords past queries. Options below.
asyncSources New in 0.6AsyncSource[]noneCommands loaded for each query, such as a server-side search. Details below.

Keep the config stable. The engines are rebuilt whenever one of searchEngine, accessControl, accessCheckMode, synonyms, frecency, groups, contextBoostWeight, t or searchHistory changes identity. Define the config once at module scope, or useMemo it, instead of writing an inline object in a component that re-renders. Callbacks, maxResults, context and asyncSources are read live, so they can be inline.

frecency

Commands you use often and recently rank higher. Usage is saved to localStorage and falls back to memory where it is unavailable: during SSR, in sandboxed iframes, when the browser blocks cookies, or when window.localStorage is null or rejects writes. To test storage, the provider writes and removes a cmdk-engine-probe key once when it mounts.

OptionDefaultDescription
enabled New in 0.6truefalse turns frecency off: nothing is stored or read, results are not ranked by past use, and no Recent group shows, even with showRecent.
storagelocalStorageA custom FrecencyStorage backend. The provider defaults to localStorage, and to memory where it is unavailable.
storageKey'cmdk-frecency'The full localStorage key, not a prefix. Every app on the origin shares the default, so namespace it per user if several people sign in on one browser. Only the provider's default storage uses it.
halfLife7 daysHow fast old usage decays: a use counts half as much after this long.
maxAge30 daysDays after its last use before an entry leaves Recent. It is removed from storage on the next recorded use.
showRecentfalseShow a Recent group when the search box is empty. It comes first, above any groups you configured.
recentCount5How many recent commands to show: the most recently used ones that are available on the current page.
recentLabel"Recent"The heading of the Recent group (the group.recent translation key).
tsx
const config = {
  frecency: {
    showRecent: true,
    recentCount: 5,
    recentLabel: 'Recent',
  },
}

To turn frecency off, set enabled: false:

tsx
const config = { frecency: { enabled: false } }

searchHistory

Off by default. When enabled, a query is recorded when the user selects a command, and kept in localStorage, or in memory where that is unavailable. The in-memory history keeps the options it was created with until the provider remounts. Read it with useSearchHistory().

OptionDefaultDescription
enabledfalseTurn recording on.
maxEntries20How many queries to keep.
minQueryLength2Shorter queries are not recorded.
storageKey'cmdk-search-history'The full localStorage key, not a prefix.

context

A command with a scope is boosted while searching when the scope matches the context. A scope entry matches the context path when it equals it or is a parent of it (/billing matches /billing/overview), or when a glob such as /billing/* covers it (below /billing, not /billing itself). It matches the context tags when it equals one of them.

asyncSources New in 0.6

Commands loaded for each query, such as a server-side search. The provider loads every source once per query for all consumers, at the root level only: it debounces, aborts stale requests on query change, close, drill-down and unmount, and reports failures per source in asyncErrors. A failing source never breaks the palette.

tsx
import type { AsyncSource } from 'cmdk-engine'

const issueSearch: AsyncSource = {
  id: 'issues',
  load: async (query, { signal }) => {
    const res = await fetch(`/api/issues?q=${encodeURIComponent(query)}`, { signal })
    const issues: { id: string; title: string }[] = await res.json()
    return issues.map((issue) => ({
      id: `issue-${issue.id}`,
      label: issue.title,
      href: `/issues/${issue.id}`,
    }))
  },
  shouldFilter: false, // the server already matched the query
  group: 'Issues',
}

export const config = { asyncSources: [issueSearch] }
OptionDefaultDescription
idrequiredKeys asyncErrors. Changing the set of ids restarts loading.
load(query, { signal })requiredReturns the items. Pass signal to fetch so stale requests are cancelled.
trigger(query)non-empty trimmed queryWhether to load for this query.
debounceMs200Delay after the last query change.
shouldFiltertruetrue: items are searched and ranked with your commands and count toward maxResults. false: the server matched them, so they are shown as returned, after the local results.
maxResults10The most items shown from this source when shouldFilter is false.
groupeach item's groupA group for every item from this source.
  • isLoading is true from the moment a trigger passes (debounce included) until every source settles. The adapters show a palette.loading row meanwhile; override it with renderLoading.
  • asyncErrors maps a source id to its last error, cleared on that source's next success.
  • Loaded items need a non-empty string id and label. Items without them (children included) are dropped, the rest still show, and asyncErrors[id] says so, for example "2 items dropped: missing label". A numeric id counts as a missing id. Other fields are checked too: non-string keywords entries are removed; an object icon, description or group that is not a React element is removed; shortcut and scope keep only their string entries; and an item whose children is not an array becomes a plain item. permissions follow the rules for registered commands: null, undefined and an empty string mean no restriction, a string is one permission, array entries become strings, and any other value hides the item.
  • when, permissions and hidden apply to loaded items too. Unfiltered items skip search, frecency, the context boost and maxResults, and their groups come after the local groups, in server order.
  • Registered commands win on duplicate ids, then earlier sources. Loaded items are never recorded in frecency.
  • load, trigger and debounceMs are read when needed, so an inline config does not restart loads. trigger runs during render (twice under StrictMode in development), so keep it pure and cheap. A trigger that passes on an empty query loads as soon as the provider mounts, even for a palette that has never been opened.

Loaded items are untrusted. Only relative, http(s):, mailto: and tel: hrefs are kept; any other href is removed when the items arrive, children included, so it never reaches window.location or a custom renderItem anchor. For deep links such as myapp://..., return an action, or an allowed href that your onNavigate maps. Only href is filtered, so render labels and descriptions as text, never as HTML.

Selecting a command

select(item), which the adapters call for you, does this in order:

  1. If the command has children, open them and stop. Nothing else runs, and it is not reported as a selection.
  2. Record frecency (not for loaded items), and the query in search history if it is enabled.
  3. Run one handler: the onSelect passed to CommandPalette or to select(); else config.onSelect; else the command's action; else onNavigate(href, item), or window.location.href = href without one.
  4. Close the palette and clear the query.

If a handler throws, or returns a rejected promise (an async action, say), the error goes to onSelectError(error, item) when you set it. The palette closes right away and does not wait for an async handler. Without onSelectError, a throw propagates and a rejection stays unhandled, as before.

tsx
import type { CommandItem } from 'cmdk-engine'

const config = {
  onSelectError: (error: unknown, item: CommandItem) => {
    console.error(item.label, 'failed', error) // or show a toast
  },
}

Because config.onSelect replaces the default handling, adding one for analytics silently stops every action and href from running. To track selections without replacing anything, use useCommandPaletteEvents below. If you do want one handler for everything, call the default yourself:

tsx
import type { CommandItem } from 'cmdk-engine'

const config = {
  onSelect: (item: CommandItem) => {
    console.log('command selected', item.id) // your analytics call
    if (item.action) item.action(item)
    else if (item.href) window.location.assign(item.href)
  },
}

Hooks

All hooks must be called in a component under CommandEngineProvider, and not in the component that renders it.

useCommandPalette()

The main hook: the current results and the controls to drive a palette.

MemberTypeDescription
searchstringThe current query.
setSearch(query: string) => voidUpdate the query.
resultsScoredItem[]The filtered, ranked results, capped by maxResults, with loaded items appended.
flatResultsScoredItem[]The same array as results. An alias; use results.
groupedResultsGroupedResult[]The results as { group, items }, in display order.
groupsCommandGroup[]The groups that have results.
isOpenbooleanWhether the palette is open.
isLoadingbooleanTrue while an async source is loading.
asyncErrorsRecord<string, Error>The last error per async source id, including dropped-item notices.
breadcrumbsCommandItem[]The sub-menu path.
depthnumber0 at the root.
open, close, toggle() => voidOpen, close or toggle. close also clears the query and returns to the root.
select(itemOrId, options?) => voidSelect a command, as described above. options.onSelect takes priority over config.onSelect.
recordUsage(commandId: string) => voidRecord frecency yourself.
drillDown, drillUp, resetPath(item) => void, () => void, () => voidMove through nested commands.

useCommandRegister(commands, deps?)

Registers commands while the component is mounted, and removes them when it unmounts. Register app-wide navigation in a layout that stays mounted, and page commands in the page.

  • Without deps, it re-registers whenever a registered field changes: ids, text, hrefs, groups, priority, disabled, hidden, keywords, permissions, access mode, shortcut, scope, text icons, children, or the result of when, which is evaluated on each render, so keep it pure. An action always calls the latest closure, so it never goes stale.
  • Element icons and meta are not compared: pass deps when they change. Pass deps, such as [flags.beta], to take control of when it re-registers.
tsx
import { useCommandRegister } from 'cmdk-engine/react'

function SettingsCommands({ plan }: { plan: string }) {
  // when is evaluated on each render, so the command appears and disappears with plan
  useCommandRegister([{ id: 'sso', label: 'SSO Settings', when: () => plan === 'enterprise' }])
  return null
}

useFrecency()

Direct access to the frecency engine: recordUsage(id), getScore(id), getRecent(count?) and clear().

useSearchHistory()

getRecent(count?), remove(query) and clear(). Needs searchHistory.enabled to have anything to read.

useCommandContext()

Returns { context, boostWeight }, read-only. To change the context, pass a new config.context to the provider.

useEngineContext(caller?)

The engine singletons, for custom UIs and advanced use: registry, search, keywords, accessFilter, frecency, groupManager, contextEngine, searchHistory, t and the current config. For example, registry.registerMany(commands) registers commands outside a component. It throws outside the provider. The optional caller is the name the error message shows; the built-in hooks pass their own.

usePaletteState(caller?)

The shared UI state, which every palette and shortcut under the provider reads and writes: isOpen, setIsOpen, search, setSearch, activePath and setActivePath. It takes the same optional caller as useEngineContext. It is the light way to open the palette from your own button, without running the results pipeline:

tsx
import { usePaletteState } from 'cmdk-engine/react'

function OpenPaletteButton() {
  const { setIsOpen } = usePaletteState()
  return <button onClick={() => setIsOpen(true)}>Search</button>
}

useCommandPaletteEvents(onEvent) New in 0.6

Reports what happens in the palette, for analytics, without any telemetry in the package. Call it once, in any component inside the provider. Without it, nothing is reported. The latest onEvent is always called, so it can be an inline function. An error it throws never breaks the palette: it is rethrown on a timer, where it reaches the console and error trackers.

tsx
import { useCommandPaletteEvents } from 'cmdk-engine/react'

function PaletteAnalytics() {
  useCommandPaletteEvents((event) => {
    if (event.type === 'search' && event.resultCount === 0) {
      console.log('no results for', event.query) // your analytics call
    }
  })
  return null
}
EventWhenFields
openThe palette opens.none
closeThe palette closes.none
searchThe results for a query settle: every async source it triggered has loaded or failed.query (trimmed, never empty), resultCount (0: nothing found)
selectA command is selected. Opening a sub-menu is not a selection.item, query, sourceId (the async source of a loaded item)
asyncErrorAn async source fails or drops items.sourceId, error

search fires for every settled query while the user types, so debounce it before sending it anywhere. New event types may be added in minor releases, so ignore types you do not know. The events are typed as CommandPaletteEvent, exported from cmdk-engine.

Translations

The built-in UI strings go through config.t(key, params?). Pass your own to localize them. getTranslationKeys() lists every key:

KeyDefaultUsed for
palette.labelCommand paletteThe accessible name of the palette and dialog.
palette.placeholderType a command or search...The input placeholder.
palette.emptyNo results found.The empty state.
palette.loadingLoading...The loading row while async sources load.
palette.listSuggestionsThe accessible name of the results list, in both adapters.
palette.closeCloseThe visually hidden close button of the Base UI dialog.
breadcrumbs.backGo backThe back button in a sub-menu.
group.recentRecentThe Recent group heading.
group.otherOtherFor your own UI. The built-in palettes do not show it; the ungrouped heading is always Other.
search.historyRecent SearchesFor your own UI. The built-in palettes do not show it.

A t that returns the key for a key it does not know shows that key as the text, so give every key you use a string. The one exception is palette.list: if t returns its key unchanged (the dictionary[key] ?? key pattern) or an empty string, the list keeps its default name, "Suggestions".

Types

All types are exported from cmdk-engine. Two names are easy to confuse: CommandEngineConfig is the provider's config prop, and CmdkEngineConfig is the CLI config file (cmdk-engine.config.ts).

ts
import type {
  CommandItem,
  CommandRegistry,
  CommandGroup,
  CommandContext,
  CommandPaletteState,
  CommandPaletteEvent,
  CommandEngineConfig, // the provider's config prop
  CmdkEngineConfig, // the CLI config file
  SearchEngine,
  ScoredItem,
  GroupedResult,
  GroupedResults,
  AccessControlProvider,
  AccessCheckMode,
  AsyncSource,
  FrecencyOptions,
  FrecencyEntry,
  FrecencyStorage,
  RecentCommandsConfig,
  SearchHistoryConfig,
  SearchHistoryEntry,
  SynonymMap,
  TranslationFn,
  RouteCommandMeta,
  Sitemap,
  SitemapRoute,
} from 'cmdk-engine'