Package Exports
- @crimson_dev/command-react
- @crimson_dev/command-react/src/styles.css
Readme
@crimson_dev/command-react
React 19 compound components for building command palettes
Headless · Accessible · Virtualized · Composable
What is this?
A complete command palette UI built on React 19 compound components. Drop-in replacement for cmdk with more features, better performance, and full TypeScript 6 support.
- 14 compound components —
Command,Input,List,Item,Group,Empty,Loading,Separator,Dialog,Highlight,Shortcut,Badge,Page,AsyncItems - React 19 native —
use(),useOptimistic,useTransition,refas prop useSyncExternalStore— zero tearing, Concurrent Mode safe- Auto-virtualization — switches on at 100+ items, GPU-composited scroll
- Radix Dialog — focus trap, portal, overlay, accessible by default
- Full ARIA — combobox pattern, live regions, keyboard navigation
- Frecency ranking — items you use often rise to the top
- Match highlighting — render exact match ranges in results
Install
pnpm add @crimson_dev/command @crimson_dev/command-reactRequires
react@>=19.0.0andreact-dom@>=19.0.0.
Quick Start
Inline palette
import { Command } from '@crimson_dev/command-react';
function CommandPalette() {
return (
<Command label="Command menu">
<Command.Input placeholder="Search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Actions">
<Command.Item value="Copy" onSelect={() => copy()}>
Copy
</Command.Item>
<Command.Item value="Paste" onSelect={() => paste()}>
Paste
</Command.Item>
</Command.Group>
<Command.Group heading="Navigation">
<Command.Item value="Go to Settings" keywords={['preferences']}>
Settings
<Command.Shortcut>Ctrl+,</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</Command>
);
}Dialog (Ctrl+K)
import { Command } from '@crimson_dev/command-react';
import { useState } from 'react';
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Command Palette</button>
<Command.Dialog open={open} onOpenChange={setOpen}>
<Command.Input placeholder="Type a command..." />
<Command.List>
<Command.Empty>Nothing found.</Command.Empty>
<Command.Item value="New File" onSelect={() => createFile()}>
New File
</Command.Item>
</Command.List>
</Command.Dialog>
</>
);
}Async items with Suspense
import { Command } from '@crimson_dev/command-react';
const fetchItems = fetch('/api/commands').then((r) => r.json());
function App() {
return (
<Command label="Command menu">
<Command.Input placeholder="Search..." />
<Command.List>
<Command.AsyncItems items={fetchItems} fallback={<Command.Loading>Loading...</Command.Loading>}>
{(items) => items.map((item) => (
<Command.Item key={item.id} value={item.value} onSelect={item.onSelect}>
{item.label}
</Command.Item>
))}
</Command.AsyncItems>
</Command.List>
</Command>
);
}Components
| Component | Description |
|---|---|
<Command> |
Root — creates state machine, provides context |
<Command.Input> |
Search input — ARIA combobox, auto-filters items |
<Command.List> |
Scrollable list container — auto-virtualizes at 100+ items |
<Command.Item> |
Selectable item — keyboard nav, pointer tracking |
<Command.Group> |
Group with heading — items grouped visually |
<Command.Empty> |
Shown when no items match the filter |
<Command.Loading> |
Shown while async items are loading |
<Command.Separator> |
Visual divider between groups |
<Command.Dialog> |
Radix Dialog wrapper — portal, overlay, focus trap |
<Command.Highlight> |
Renders match ranges with <mark> tags |
<Command.Shortcut> |
Displays formatted keyboard shortcut |
<Command.Badge> |
Status badge for items |
<Command.Page> |
Multi-page navigation |
<Command.AsyncItems> |
Suspense-powered async item loading |
Hooks
| Hook | Description |
|---|---|
useCommandState() |
Read full state or a selector slice |
useCommandState(s => s.search) |
Derived state — only re-renders on change |
useCommand(machine) |
Low-level: connect a machine to React |
useRegisterItem(value, opts) |
Imperatively register an item |
useRegisterGroup(heading, opts) |
Imperatively register a group |
useVirtualizer(opts) |
Standalone virtualizer hook |
Styling
Import the optional base CSS or write your own:
import '@crimson_dev/command-react/src/styles.css';All components use data-command-* attributes for styling:
[data-command-root] { /* root container */ }
[data-command-input] { /* search input */ }
[data-command-list] { /* scrollable list */ }
[data-command-item] { /* each item */ }
[data-command-item][data-active] { /* highlighted item */ }
[data-command-item][data-disabled] { /* disabled item */ }
[data-command-empty] { /* no results message */ }
[data-command-group] { /* group wrapper */ }
[data-command-separator] { /* divider line */ }
[data-command-dialog] { /* dialog content */ }
[data-command-overlay] { /* dialog backdrop */ }Migrating from cmdk
Use the automated codemod:
npx @crimson_dev/command-codemod --transform import-rewrite ./src
npx @crimson_dev/command-codemod --transform forward-ref ./src
npx @crimson_dev/command-codemod --transform data-attrs ./src
npx @crimson_dev/command-codemod --transform should-filter ./srcLinks
License
MIT