Architecture Internals
Looking for the high-level tour?
Start with the Architecture Overview — this page drills into the monorepo package graph and per-subsystem internals.
modern-cmdk is built as a three-layer architecture: a framework-agnostic core engine, a React adapter, and an optional WASM search acceleration layer.
Package Dependency Graph
Core Engine (modern-cmdk)
The core is a pure TypeScript state machine with no framework dependency and no hard DOM dependency (DOM APIs are feature-detected and SSR-safe). It manages search, filtering, keyboard navigation, frecency ranking, and keyboard shortcuts.
The machine has no named search or navigation states. It holds one CommandState record (read it with machine.getState()), and each event updates fields of it: SEARCH_CHANGE updates search, filteredIds, groupedIds, filteredCount and activeId; NAVIGATE and ITEM_ACTIVATE update activeId; ITEM_SELECT changes no state field (it calls the item's and the machine's onSelect, emits select and records the selection in the frecency engine); and PAGE_PUSH / PAGE_POP update page and pageStack. Only open behaves like a state.
Key Subsystems
| Module | Responsibility |
|---|---|
machine.ts | State transitions, subscriber notification |
registry.ts | Item/group registration with set operation helpers (union/intersection/difference) |
search/default-scorer.ts | Fuzzy matching and scoring |
search/index.ts | Search engine factory, pluggable scorer interface |
frecency/index.ts | Time-based decay buckets (Date.now) |
frecency/idb-storage.ts | IndexedDB persistence via idb-keyval |
keyboard/parser.ts | Shortcut string parsing |
keyboard/matcher.ts | Event matching + conflict detection via groupBy helper |
utils/scheduler.ts | Update batching -- requestAnimationFrame in browsers, microtask in Node.js |
utils/event-emitter.ts | Type-safe pub/sub with Disposable cleanup |
Data Flow: Search Pipeline
React Adapter (modern-cmdk/react)
The React adapter exposes 15 compound components (the Command root plus 14 subcomponents) that bind to the core state machine:
React 19 Integration Points
useSyncExternalStore— Subscribe to the core state machine without tearinguseTransition— Wraps only the optimistic active-item update;Command.Inputkeeps an urgent draft value and the machine catches up on its scheduler tick, soisPendingnever reflects a searchuseOptimistic— OptimisticactiveIdupdates for instant navigation feedbackrefas prop — NoforwardRefneeded (React 19 native)- Activity API —
CommandActivityfor keep-alive state preservation
Virtualization
The list component auto-virtualizes when filtered items exceed a threshold. The virtualizer uses:
ResizeObserverfor dynamic item height measurementrequestIdleCallbackfor deferred measurement- the
translateproperty for transform-based positioning, off the layout path content-visibility: autofor off-screen rendering skip
WASM Search (modern-cmdk-search-wasm)
Optional Rust-compiled trigram index for large datasets (10K+ items).
Two Execution Modes
| Mode | Function | Thread | Use Case |
|---|---|---|---|
| Main thread | createWasmSearchEngine() | Main | Simple setup, designed for up to ~50K items (a design target) |
| Worker thread | createWorkerSearchEngine() | Web Worker | Large datasets, non-blocking UI |
Both implement the SearchEngine interface from the core package and support AsyncDisposable for await using cleanup.
Resource Management
All engines implement Disposable or AsyncDisposable for automatic cleanup:
import { createSearchEngine, itemId } from 'modern-cmdk';
import type { CommandItem } from 'modern-cmdk';
import { createWasmSearchEngine } from 'modern-cmdk-search-wasm';
const items: readonly CommandItem[] = [
{ id: itemId('copy'), value: 'Copy' },
{ id: itemId('paste'), value: 'Paste' },
];
// Synchronous — using
{
using engine = createSearchEngine();
engine.index(items);
engine.search('query', items).toArray();
} // engine[Symbol.dispose]() called automatically
// Asynchronous — await using
{
await using engine = await createWasmSearchEngine();
engine.index(items);
// ...
} // engine[Symbol.asyncDispose]() called automaticallyBuild Output
packages/modern-cmdk/tsdown.config.ts holds three tsdown configs:
- The core (
dist/core/index.mjs, with declarations). - The React adapter: two entries,
modern-cmdk/reactandmodern-cmdk/react/dialog, in one config so the code both use (context, hooks,CommandDialog) is emitted once as a shared chunk indist/react. The core is external: the adapter imports it asmodern-cmdk, so a bundle holds one core, not a second copy inlined into the adapter.radix-uistays external too, and the two React outputs (react/index.mjs,react/dialog.mjs) begin with'use client': the banner comes from this config, and no file undersrc/reactcarries the directive.styles.cssis copied todist/react/styles.css. - The codemod (
dist/codemod): the deprecatedmodern-cmdkbin. Its source lives in the sibling packagepackages/modern-cmdk-codemod(binmodern-cmdk-codemod); this config buildscli.mjs, the worker and the transforms from that source, bundled with jscodeshift and its parser tree (ten packages), behind the shimdist/codemod/bin.mjs, which warns once and keeps the 1.x exit codes and 1.4's excludes until 2.0.modern-cmdkshipsTHIRD-PARTY-NOTICES.mdfor the bundled code (pnpm check:notices). Neithermodern-cmdknormodern-cmdk-codemodships declarations for the codemod, because it is a CLI and not an importable API.
sideEffects in packages/modern-cmdk/package.json is ["./dist/react/styles.css"] only. Command.Dialog is a member of the Command compound, so every modern-cmdk/react import loads radix-ui, and radix-ui is a required peer dependency (>=1.4.0 <2.0.0) through 1.x. See Server-Side Rendering for the measured consequence for bundle size.
Language Features & Editions
| Feature | Edition | Where Used | Native/Helper |
|---|---|---|---|
| Iterator Helpers | ES2025 | Search pipelines, registry operations, frecency bonuses | Native |
Symbol.dispose / Symbol.asyncDispose (using / await using) | ESNext (TS lib esnext.disposable) | Engines, storages and the machine implement the protocol, so consumers can write using | Native |
Promise.withResolvers | ES2024 | Worker communication in the unpublished modern-cmdk-search-wasm package | Native |
RegExp.escape | ES2025 | Not used | -- |
Set methods (union, intersection, difference) | ES2025 | Registry union/intersection/difference for group operations | Helper functions (set-ops.ts) |
Map.groupBy / Object.groupBy | ES2024 | Shortcut conflict detection, item grouping | Helper functions (group-by.ts) |
Date.now() | ES5 | Frecency timestamps, state lastUpdated | Native (replaces Temporal) |
String.prototype.toWellFormed | ES2024 | Branded ID creation | Helper function (string-wellformed.ts) |