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:
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
actionorhref, andonSelectis not called for it. An emptychildrenarray counts as a leaf. - The palette lists only the children, and search covers only that level.
when,permissionsandhiddenapply 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
childrenshow 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:
| Member | Description |
|---|---|
| breadcrumbs | The commands you drilled through, outermost first (CommandItem[]). |
| depth | 0 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. |
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:
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 field | Description |
|---|---|
| id | Matches the group string on commands. |
| label | The heading text. |
| priority | Optional. Higher groups are listed first when the query is empty. |
| icon | Optional. The built-in components do not render it; renderGroupHeading(group) receives it. |
- A
groupyou did not define still shows, labelled with thegroupstring. - 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 withfrecency.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, leaveshowRecentoff, or render your own list fromgroupedResults.