JSPM

@crimson_dev/command-react

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q36091F
  • License MIT

React 19 adapter for @crimson_dev/command

Package Exports

  • @crimson_dev/command-react
  • @crimson_dev/command-react/src/styles.css

Readme

@crimson_dev/command-react

@crimson_dev/command-react

React 19 compound components for building command palettes
Headless · Accessible · Virtualized · Composable

npm downloads bundle size license


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 componentsCommand, Input, List, Item, Group, Empty, Loading, Separator, Dialog, Highlight, Shortcut, Badge, Page, AsyncItems
  • React 19 nativeuse(), useOptimistic, useTransition, ref as 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-react

Requires react@>=19.0.0 and react-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 ./src

License

MIT