Nested pages in cmdk
Nested pages let one command open a more specific list of commands, such as picking a project after choosing "Search projects".
How it works
- Keep a stack of pages in state.
pagesis an array and the last entry is the current page. Selecting "Search projects…" or "Join a team…" pushes a page name withonSelect. [1] - Render items for the current page. The list renders a different set of
Command.Itemelements depending onpage. A small breadcrumb above the input shows where you are. [1] - Go back with the keyboard.
onKeyDownonCommandpops the last page on Escape, or on Backspace when the search is empty. The input is controlled withvalueandonValueChangeso the handler can read the search text. [1]
Run and edit it
It runs in your browser, in a sandboxed frame that cannot see this site; the packages load from esm.sh only after you press Run.
import { useState } from 'react'
import { Command } from 'cmdk'
const css = `
.wrap { min-height: 100vh; display: grid; place-items: center; background: #111; font-family: system-ui, sans-serif; }
[cmdk-root] { width: 380px; background: #1c1c22; border: 1px solid #333; border-radius: 12px; color: #eee; overflow: hidden; }
[cmdk-input] { width: 100%; box-sizing: border-box; padding: 14px 16px; border: 0; border-bottom: 1px solid #333;
background: transparent; color: #fff; font-size: 15px; outline: none; }
[cmdk-list] { max-height: 240px; overflow: auto; padding: 6px; }
[cmdk-item] { padding: 9px 10px; border-radius: 8px; font-size: 14px; cursor: pointer; }
[cmdk-item][data-selected="true"] { background: #3b3b4f; }
.crumb { padding: 8px 14px 0; font-size: 12px; color: #8a8aff; }
`
export default function App() {
const [search, setSearch] = useState('')
const [pages, setPages] = useState([])
const page = pages[pages.length - 1]
const go = (p) => { setPages([...pages, p]); setSearch('') }
return (
<div className="wrap">
<style>{css}</style>
<Command
label="Nested menu"
onKeyDown={(e) => {
// Escape, or Backspace on an empty search, goes to the previous page
if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) {
e.preventDefault()
setPages((pages) => pages.slice(0, -1))
}
}}
>
<div className="crumb">Home{pages.map((p) => ' / ' + p).join('')}</div>
<Command.Input value={search} onValueChange={setSearch} placeholder="Search..." autoFocus />
<Command.List>
{!page && (
<>
<Command.Item onSelect={() => go('projects')}>Search projects…</Command.Item>
<Command.Item onSelect={() => go('teams')}>Join a team…</Command.Item>
</>
)}
{page === 'projects' && (
<>
<Command.Item>Project A</Command.Item>
<Command.Item>Project B</Command.Item>
</>
)}
{page === 'teams' && (
<>
<Command.Item>Team 1</Command.Item>
<Command.Item>Team 2</Command.Item>
</>
)}
</Command.List>
</Command>
</div>
)
}
Press Enter on "Search projects…", then press Backspace on the empty input to return to the first page. Code written by AgentGid [1].
Press Enter on "Search projects…", then press Backspace on the empty input to return to the first page. Code written by AgentGid [1].Checked Oct 12, 2026: it starts without errors in Chromium with cmdk 1.1.1, react 19.3.0, react-dom 19.3.0. Needs WebGL.
More cmdk examples
Command menu in cmdk Cmd+K command dialog in cmdk Custom filter and keywords in cmdk useCommandState in cmdk
Sources
Explanations written by AgentGid from the official docs; the numbers in brackets point here. The code marked "written by AgentGid" is ours and free to copy.