Skip to content

modern-cmdk/react ​

React 19 compound component adapter for modern-cmdk. The shipped components are client-only (dist/react/index.mjs and dist/react/dialog.mjs begin with "use client"; the build adds it, the source files do not carry it) and accept ref as a prop (no forwardRef).

Installation ​

bash
pnpm add modern-cmdk
bash
npm install modern-cmdk

Every modern-cmdk/react import needs react and react-dom (>=19.0.0 <21.0.0) and radix-ui (>=1.4.0 <2.0.0). react and react-dom are optional peers of modern-cmdk, but radix-ui requires them, so npm 7+ and pnpm install them with modern-cmdk/react; install them explicitly to pin the version: pnpm add react react-dom radix-ui.

<Command> ​

Root component that creates a command palette state machine and provides context to all child components.

tsx
import type { ReactNode } from 'react';
import type { CommandItem } from 'modern-cmdk';
import { Command } from 'modern-cmdk/react';

// A custom filter: return a score, or false to hide the item.
const customFilterFn = (item: CommandItem, query: string): number | false =>
  item.value.toLowerCase().includes(query.toLowerCase()) ? 1 : false;

function Palette({ children }: { children: ReactNode }) {
  return (
    <Command
      label="Command Menu"
      filter={customFilterFn}
      frecency={{ enabled: true }}
      loop={true}
      onSelect={(id) => console.log(id)}
    >
      {children}
    </Command>
  );
}

Props ​

PropTypeDefaultDescription
childrenReactNode--Child components
classNamestring--Class name for the root <div>
labelstring'Command palette'Accessible label (aria-label)
itemsCommandItem[][]Pre-registered items
groupsCommandGroup[][]Pre-registered groups
filterFunction | falsebuilt-inCustom filter function, or false to disable
searchSearchEnginebuilt-inPluggable search engine (e.g., WASM)
frecencyFrecencyOptions--Frecency ranking configuration; ranking is on only with enabled: true (enabled is optional, default false)
loopbooleantrueWhether keyboard navigation wraps
openbooleanfalseInitial open state
onSelect(id: ItemId) => void--Fired when an item is selected
onActiveChange(id: ItemId | null) => void--Fired when the active item changes
onOpenChange(open: boolean) => void--Fired when open state changes
onSearchChange(query: string) => void--Fired when the search query changes

Data Attributes ​

AttributeValueDescription
data-command-root""Always present on root
data-command-state"open" | "closed"Current open/closed state

ARIA ​

AttributeValue
role"search"
aria-labelValue of label prop

<Command.Input> ​

Search input with ARIA combobox role. Manages the search query and connects to the listbox.

tsx
import { Command } from 'modern-cmdk/react';

const input = (
  <Command.Input
    placeholder="Type a command..."
    onValueChange={(value) => console.log(value)}
  />
);

Props ​

Extends ComponentPropsWithRef<'input'> (excluding value, onChange, type, role).

PropTypeDefaultDescription
refRef<HTMLInputElement>--Ref to the input element
placeholderstring'Search...'Placeholder text
onValueChange(value: string) => void--Callback when input value changes

Data Attributes ​

AttributeDescription
data-command-inputAlways present

ARIA ​

AttributeValue
role"combobox"
aria-expandedtrue when filteredCount > 0
aria-controlsID of the <Command.List>
aria-activedescendantID of the active item
aria-autocomplete"list"
aria-labelValue of root label prop

<Command.List> ​

Scrollable result list with built-in virtualization, aria-live announcements, and scroll-to-active behavior.

tsx
import type { ReactNode } from 'react';
import { Command } from 'modern-cmdk/react';

function Results({ children }: { children: ReactNode }) {
  return (
    <Command.List estimateSize={48} overscan={10}>
      {children}
    </Command.List>
  );
}

Props ​

Extends ComponentPropsWithRef<'div'>.

PropTypeDefaultDescription
refRef<HTMLDivElement>--Ref to the scroll container
virtualizebooleanunset (auto)Unset: virtualizes above 100 filtered items and stops again at 50 or fewer. true forces it on; false disables it
estimateSizenumber44Estimated item height in pixels
overscannumber8Items rendered beyond the visible viewport (each direction)

Data Attributes ​

AttributeDescription
data-command-listAlways present
data-command-list-innerInner container (non-virtual mode)
data-command-list-virtualInner container (virtual mode)

ARIA ​

AttributeValue
role"listbox"
aria-labelValue of root label prop
aria-busytrue when loading

CSS Custom Properties ​

PropertyDescription
--command-list-heightMeasured inner height (set via ResizeObserver)

<Command.Item> ​

Individual command item. Renders only when it passes the current filter.

tsx
import { Command } from 'modern-cmdk/react';

// Your app's navigation (app-owned).
function openSettings(): void {
  location.assign('/settings');
}

const settingsItem = (
  <Command.Item
    value="settings"
    keywords={['preferences', 'config']}
    shortcut="Mod+,"
    onSelect={() => openSettings()}
    disabled={false}
  >
    Settings
    <Command.Shortcut shortcut="Mod+," />
  </Command.Item>
);

Props ​

Extends ComponentPropsWithRef<'div'> (excluding onSelect, value).

PropTypeDefaultDescription
refRef<HTMLDivElement>--Ref to the item element
valuestringRequiredDisplay text and primary search target
keywordsreadonly string[]--Additional searchable terms
onSelect() => void--Callback when the item is selected; called with no argument (the item value arrives as the first argument in 2.0)
disabledbooleanfalseWhether the item is disabled
shortcutstring--Keyboard shortcut string (e.g., "Mod+K")
forceIdstring--Override the auto-generated item ID

Data Attributes ​

AttributeConditionDescription
data-command-itemAlwaysIdentifies the element as a command item
data-activeWhen activeCurrently highlighted item
data-disabledWhen disabledDisabled item
data-valueAlwaysThe value prop

ARIA ​

AttributeValue
role"option"
aria-selectedtrue when active
aria-disabledtrue when disabled
aria-keyshortcutsShortcut string (if provided)

<Command.Group> ​

Groups related items with an optional heading. Automatically hidden when no items in the group pass the filter.

tsx
import { Command } from 'modern-cmdk/react';

const navigation = (
  <Command.Group heading="Navigation" priority={1}>
    <Command.Item value="home">Home</Command.Item>
    <Command.Item value="about">About</Command.Item>
  </Command.Group>
);

Props ​

Extends ComponentPropsWithRef<'div'>.

PropTypeDefaultDescription
refRef<HTMLDivElement>--Ref to the group container
headingstring--Heading text displayed above the group
prioritynumber--Sort priority (lower = higher in list)
forceIdstring--Override the auto-generated group ID

Data Attributes ​

AttributeDescription
data-command-groupGroup container
data-command-group-headingHeading element
data-command-group-itemsItems container

ARIA ​

AttributeValue
role"group"
aria-labelledbyID of the heading element (if heading is provided)

<Command.Empty> ​

Renders when no items match the current search query and the list is not loading.

tsx
import { Command } from 'modern-cmdk/react';

const empty = <Command.Empty>No results found.</Command.Empty>;

Extends ComponentPropsWithRef<'div'>. Data attribute: data-command-empty. ARIA: role="status", aria-live="polite".


<Command.Loading> ​

Renders while state.loading is true or the optimistic active-item transition is pending (isPending). state.loading is true while any <Command.AsyncItems> is pending (the machine's LOADING event), so the component needs no props to show async loading; a search never sets isPending. The loading prop is an override, not a requirement. Keep it on a <Command.Loading> used as an AsyncItems fallback, so the element renders on the fallback's first render.

tsx
import { Command } from 'modern-cmdk/react';

function Status({ isSearching }: { isSearching: boolean }) {
  return <Command.Loading loading={isSearching}>Searching...</Command.Loading>;
}

Extends ComponentPropsWithRef<'div'>.

PropTypeDefaultDescription
loadingbooleanauto-detectedOverride loading state detection (state.loading or isPending)

Data attribute: data-command-loading. ARIA: role="status", aria-busy, aria-live="assertive".


<Command.Separator> ​

Visual separator between groups or items.

tsx
import { Command } from 'modern-cmdk/react';

const separator = <Command.Separator />;

Extends ComponentPropsWithRef<'div'>. Data attribute: data-command-separator. ARIA: role="none".


<Command.Dialog> ​

Full dialog implementation using Radix Dialog with focus trap, overlay, @starting-style animations, and controlled open/close state. Until the dialog-token fix ships (1.8.0) the enter and exit animations do not run out of the box; see Theming.

The same component is exported as CommandDialog, with its CommandDialogProps type, from the modern-cmdk/react/dialog subpath. Command.Dialog on the modern-cmdk/react barrel stays in 1.x. Both need radix-ui installed:

tsx
import { CommandDialog, type CommandDialogProps } from 'modern-cmdk/react/dialog';
tsx
'use client';

import { Command } from 'modern-cmdk/react';
import { useState, useEffect } from 'react';

function CommandPalette() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen((o) => !o);
      }
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);

  return (
    <Command.Dialog open={open} onOpenChange={setOpen} label="Command Menu">
      <Command.Input placeholder="Search commands..." />
      <Command.List>
        <Command.Empty>No results.</Command.Empty>
        <Command.Item value="home" onSelect={() => setOpen(false)}>Home</Command.Item>
      </Command.List>
    </Command.Dialog>
  );
}

Props ​

Inherits all CommandMachineOptions props, plus:

PropTypeDefaultDescription
childrenReactNode--Dialog content
classNamestring--Class name for the Dialog.Content wrapper
overlayClassNamestring--Class name for the overlay
contentClassNamestring--Class name for the inner content container
labelstring'Command palette'Accessible label
descriptionstring'Type a command or search...'Accessible description for screen readers
openboolean--Controlled open state (omit for uncontrolled mode)
onOpenChange(open: boolean) => void--Called when open state changes
containerHTMLElement | nulldocument.bodyPortal container element

Data Attributes ​

AttributeElementValue
data-command-overlayOverlay""
data-command-dialogDialog container""
data-command-dialog-contentInner content""
data-stateOverlay and dialog"open" | "closed"

<Command.Highlight> ​

Renders text with highlighted match ranges from the search scorer.

tsx
import { Command } from 'modern-cmdk/react';

const highlight = (
  <Command.Highlight
    text="Application Settings"
    ranges={[[0, 3], [12, 15]]}
    highlightClassName="my-highlight"
  />
);
PropTypeDefaultDescription
textstringRequiredFull text to display
ranges[number, number][]RequiredMatch ranges from search scorer
highlightClassNamestring--Class name for <mark> elements

Data attribute: data-command-highlight on each <mark> element.


<Command.Shortcut> ​

Renders a platform-aware keyboard shortcut label. On macOS displays glyphs, on Windows/Linux displays text labels.

tsx
import { Command } from 'modern-cmdk/react';

const shortcut = <Command.Shortcut shortcut="Mod+Shift+K" />;

Extends ComponentPropsWithRef<'kbd'>.

PropTypeDefaultDescription
shortcutstring--Shortcut string to parse and display
childrenReactNode--Manual content (used if shortcut is not provided)

Data attribute: data-command-shortcut.


<Command.Badge> ​

Category or type badge for items. Extends ComponentPropsWithRef<'span'>. Data attribute: data-command-badge.


<Command.Page> ​

Renders content conditionally based on the active page. Used for multi-page command palette flows.

PropTypeDefaultDescription
idstringRequiredPage identifier
childrenReactNode--Page content

Data attributes: data-command-page, data-command-page-id.


<Command.AsyncItems> ​

Loads items from an async source using React 19 use() hook with Suspense integration.

tsx
import { itemId } from 'modern-cmdk';
import type { CommandItem } from 'modern-cmdk';
import { Command } from 'modern-cmdk/react';

// Your data source (app-owned).
async function fetchItems(): Promise<CommandItem[]> {
  return [{ id: itemId('home'), value: 'Home' }];
}

const asyncItems = (
  <Command.AsyncItems
    items={fetchItems()}
    fallback={<Command.Loading loading>Loading...</Command.Loading>}
  >
    {(items) => items.map((item) => (
      <Command.Item key={item.id} value={item.value}>{item.value}</Command.Item>
    ))}
  </Command.AsyncItems>
);
PropTypeDefaultDescription
itemsPromise<readonly CommandItem[]>RequiredAsync item source
fallbackReactNode--Fallback during loading (passed to <Suspense>)
children(items: CommandItem[]) => ReactNodeRequiredRender function for resolved items

<Command.Activity> ​

Wraps content in React's Activity API (stable since React 19.2) for state preservation. When mode="hidden", the component tree is preserved in memory without rendering. Falls back to conditional rendering if the Activity API is unavailable.

PropTypeDefaultDescription
mode'visible' | 'hidden'RequiredWhether the content is rendered
childrenReactNodeRequiredContent to preserve

<CommandErrorBoundary> ​

Error boundary for graceful failure recovery in command subtrees. Exported as a named export (not attached to Command).

tsx
import { Command, CommandErrorBoundary } from 'modern-cmdk/react';

const guarded = (
  <CommandErrorBoundary
    fallback={(error, reset) => (
      <div>
        Palette failed: {error.message}{" "}
        <button type="button" onClick={reset}>
          Retry
        </button>
      </div>
    )}
    onError={(error, info) => console.error(error, info.componentStack)}
  >
    <Command>{/* ... */}</Command>
  </CommandErrorBoundary>
);
PropTypeDefaultDescription
childrenReactNodeRequiredSubtree to guard
fallbackReactNode | ((error: Error, reset: () => void) => ReactNode)nullRendered when an error is caught; a function receives the error and reset, which clears the caught error so the children render again
resetKeysreadonly unknown[]--Resets the boundary when an entry changes between renders (Object.is, by position, or a different length)
onError(error: Error, errorInfo: ErrorInfo) => void--Error reporting callback

Hooks ​

useCommandState(selector?) ​

Subscribe to command state via useSyncExternalStore. Call with no arguments for the full CommandState, or pass a selector to re-render only when the selected value changes:

tsx
import { useCommandState } from 'modern-cmdk/react';

function ResultCount() {
  const count = useCommandState((state) => state.filteredCount);
  return <span>{count} results</span>;
}

useCommand(machine) ​

Low-level hook that binds a CommandMachine to React via useSyncExternalStore and useOptimistic, with a transition around the optimistic active-item setter only. <Command> uses it internally; reach for it only when building a custom root.

Returns UseCommandReturn:

FieldTypeDescription
stateCommandStateCurrent state snapshot
isPendingbooleantrue while the optimistic active-item transition is pending; never reflects a search
updateSearch(query: string) => voidSend a SEARCH_CHANGE event to the machine; it is applied on the machine's scheduler tick, not inside a transition
setOptimisticActiveId(id: ItemId | null) => voidOptimistically highlight an item
filteredIdSetReadonlySet<ItemId>O(1) membership set for filtered items
idCommandRootIdStable SSR-safe root ID (from useId)

useRegisterItem(value, options?) ​

Register a command item imperatively — auto-deregisters on unmount. Returns the branded ItemId. Options: keywords, groupId, shortcut, disabled, onSelect, data, forceId.

useRegisterGroup(heading?, options?) ​

Register a command group imperatively — auto-deregisters on unmount. Returns the branded GroupId. Options: priority, forceId.

useVirtualizer(options) ​

Lightweight list virtualizer built on ResizeObserver and scroll tracking. <Command.List> uses it internally.

OptionTypeDefaultDescription
countnumberRequiredTotal item count
estimateSizenumberRequiredEstimated item height (px)
overscannumber8Extra items rendered per direction
scrollElementHTMLElement | nullRequiredScroll container
enabledbooleantrueToggle virtualization

Returns { virtualItems, totalSize, scrollToIndex, measureElement }.

useCommandDevtools(label?) ​

Exposes palette internals (registered/filtered item counts, query, active ID, page stack, open/loading state) on a global registry (__CRIMSON_COMMAND_DEVTOOLS__) for inspection from the browser console. Development-only — the effect is gated on process.env.NODE_ENV and inert in production builds. Call it from a component rendered inside <Command>.

createKeydownHandler(machine, getState) ​

Factory for the root keydown handler — ArrowUp/ArrowDown/Home/End navigation, Enter to select, Escape to close, Backspace on an empty query to pop the page stack. Used internally by <Command>; exported for custom roots.


All Data Attributes ​

AttributeComponent
[data-command-root]Command
[data-command-state]Command
[data-command-input]Command.Input
[data-command-list]Command.List
[data-command-list-inner]Command.List (non-virtual)
[data-command-list-virtual]Command.List (virtual)
[data-command-item]Command.Item
[data-active]Command.Item (active)
[data-disabled]Command.Item (disabled)
[data-value]Command.Item
[data-command-group]Command.Group
[data-command-group-heading]Command.Group
[data-command-group-items]Command.Group
[data-command-empty]Command.Empty
[data-command-loading]Command.Loading
[data-command-separator]Command.Separator
[data-command-dialog]Command.Dialog
[data-command-overlay]Command.Dialog
[data-command-dialog-content]Command.Dialog
[data-command-highlight]Command.Highlight
[data-command-shortcut]Command.Shortcut
[data-command-badge]Command.Badge
[data-command-page]Command.Page
[data-command-virtual-item]Reserved hook; no component emits it (for custom rows built on useVirtualizer, see the Theming guide)
[data-command-scroll-indicator]Reserved hook; no component emits it (see the Theming guide)
[data-command-aria-live]Live region (screen reader only)

All CSS Custom Properties ​

PropertySyntaxDefaultDescription
--command-list-height<length>0pxMeasured list inner height
--command-count<integer>0Filtered item count
--command-overlay-opacity<number>0Overlay backdrop opacity
--command-dialog-scale<number>0.96Dialog entrance scale
--command-dialog-translate<length>8pxDialog entrance translate offset