Nested Commands and Groups

Two ways to organize a long command list: sub-menus, where selecting a command opens its children, and groups, which put commands under headings.

Nested commands

Give a command children to make a sub-menu. Selecting it opens its children instead of running it:

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

const setTheme = (theme: string) => document.documentElement.setAttribute('data-theme', theme)

function ThemeCommands() {
  useCommandRegister([
    {
      id: 'theme',
      label: 'Change theme',
      children: [
        { id: 'theme-light', label: 'Light', action: () => setTheme('light') },
        { id: 'theme-dark', label: 'Dark', action: () => setTheme('dark') },
      ],
    },
  ])
  return null
}
  • A command with children never runs its own action or href, and onSelect is not called for it. An empty children array counts as a leaf.
  • The palette lists only the children, and search covers only that level. when, permissions and hidden apply to them as at the root.
  • Backspace in an empty input, or the back button in the breadcrumbs, goes up one level. Closing the palette returns to the root.
  • Opening a sub-menu is not recorded in frecency or search history; running a child is.
  • Async sources load at the root level only, and their loads are cancelled when you drill in.
  • Children can have children, to any depth. Each level adds a crumb to the trail.
  • Changes to the registered command's children show up while you are inside the sub-menu. A parent that is no longer registered keeps showing the children it had.

Breadcrumbs and custom UIs

Both adapters show a breadcrumb trail inside a sub-menu and a chevron on commands that have children. Replace the trail with renderBreadcrumbs(crumbs, onBack), and style the default one with data-cmdk-engine-breadcrumbs and the other attributes on the Styling page. If you build your own UI, useCommandPalette() returns the state and the controls:

MemberDescription
breadcrumbsThe commands you drilled through, outermost first (CommandItem[]).
depth0 at the root, 1 inside the first sub-menu, and so on.
drillDown(item)Open the children of a command. Does nothing if it has none.
drillUp()Go up one level. Also clears the search box.
resetPath()Go back to the root. Also clears the search box.
tsx
import { useCommandPalette } from 'cmdk-engine/react'

function MenuTrail() {
  const { breadcrumbs, drillUp, resetPath } = useCommandPalette()
  if (breadcrumbs.length === 0) return null
  return (
    <nav aria-label="Sub-menu">
      <button onClick={resetPath}>All commands</button>
      {breadcrumbs.map((crumb) => (
        <span key={crumb.id}> / {crumb.label}</span>
      ))}
      <button onClick={drillUp}>Back</button>
    </nav>
  )
}

Groups

A command's group string puts it under a heading. Without any config, each distinct group becomes a heading with the same text, and commands without a group go under "Other", always last. Set groups in the provider config to choose labels and order:

tsx
import { CommandEngineProvider, useCommandRegister } from 'cmdk-engine/react'

const config = {
  groups: [
    { id: 'navigation', label: 'Go to', priority: 10 },
    { id: 'actions', label: 'Actions', priority: 5 },
  ],
}

function Commands() {
  useCommandRegister([
    { id: 'home', label: 'Home', href: '/', group: 'navigation' },
    { id: 'invite', label: 'Invite teammate', action: () => {}, group: 'actions' },
  ])
  return null
}

export function Root({ children }: { children: React.ReactNode }) {
  return (
    <CommandEngineProvider config={config}>
      <Commands />
      {children}
    </CommandEngineProvider>
  )
}
CommandGroup fieldDescription
idMatches the group string on commands.
labelThe heading text.
priorityOptional. Higher groups are listed first when the query is empty.
iconOptional. The built-in components do not render it; renderGroupHeading(group) receives it.
  • A group you did not define still shows, labelled with the group string.
  • With an empty query, defined groups are listed by priority (higher first), then the other groups in the order their first command appears. While searching, groups are ordered by their best match instead.
  • Inside a group, commands are ordered by score. maxResults (default 50) caps the total number of results across groups.
  • The Recent group (frecency.showRecent) comes first, above your configured groups, and is labelled with frecency.recentLabel. It lists the most recently used commands that are available on the current page. The palette highlights the first item when it opens, so with groups configured that is the most recent command, and pressing Enter right after opening runs it. To keep your configured groups on top, leave showRecent off, or render your own list from groupedResults.