Skip to main content
Gremorie

Command

Keyboard-first command palette built on cmdk. Inline picker for Combobox patterns or floating Cmd+K dialog for global navigation.

Overview

Command is the right primitive for keyboard-first pickers and command palettes: inline searchable selects (Combobox), action launchers, file or doc finders. Use Command directly when the palette lives in place (typically inside a Popover for the Combobox pattern); use CommandDialog for the canonical floating Cmd+K palette pattern used by GitHub, Linear, Vercel, and Notion.

Always include a keyboard hint in the input placeholder ("Type a command or search..."), close on Escape, focus the input on open, and group items by intent (Navigation, Actions, Recent).

Preview

'use client';import {  Command,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,  CommandSeparator,} from '@gremorie/rx-overlays';export function CommandPreview() {  return (    <Command className="rounded-lg border max-w-md">      <CommandInput placeholder="Type a command or search..." />      <CommandList>        <CommandEmpty>No results found.</CommandEmpty>        <CommandGroup heading="Suggestions">          <CommandItem>Calendar</CommandItem>          <CommandItem>Search emoji</CommandItem>          <CommandItem>Calculator</CommandItem>        </CommandGroup>        <CommandSeparator />        <CommandGroup heading="Settings">          <CommandItem>Profile</CommandItem>          <CommandItem>Billing</CommandItem>          <CommandItem>Settings</CommandItem>        </CommandGroup>      </CommandList>    </Command>  );}

Anatomy

Command                       cmdk root list container
├─ CommandInput               search field with a leading magnifier
└─ CommandList                scrollable results region
   ├─ CommandEmpty            message shown when nothing matches
   ├─ CommandGroup            labelled cluster of items
   │  └─ CommandItem          a selectable row
   │     └─ CommandShortcut   right-aligned keyboard hint
   └─ CommandSeparator        divider between groups

Installation

bash npx gremorie@latest add rx-command
bash pnpm dlx gremorie@latest add rx-command
bash yarn dlx gremorie@latest add rx-command
bash bunx --bun gremorie@latest add rx-command

Usage

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@gremorie/rx-overlays";

export function Example() {
  return (
    <Command className="rounded-lg border max-w-md">
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search emoji</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>Profile</CommandItem>
          <CommandItem>Billing</CommandItem>
          <CommandItem>Settings</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  );
}

The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.

API

<Command>

Extends cmdk's Command root. Notable props:

PropTypeDefaultDescription
valuestring-Controlled selected item value.
onValueChange(value: string) => void-Fired when selection changes via arrow keys or input.
shouldFilterbooleantrueDisable to control filtering yourself (e.g. server-side search).
filter(value: string, search: string, keywords?: string[]) => numberdefault fuzzy matchCustom filter scoring (return 0 to hide).
loopbooleanfalseWrap arrow-key navigation from last to first item.

All other props from cmdk's Command are forwarded.

<CommandInput>

Search input that drives filtering. Wrapped in a flex row with a leading search icon.

PropTypeDefaultDescription
valuestring-Controlled input value.
onValueChange(value: string) => void-Fired when the input changes.

Extends cmdk's Command.Input.

<CommandList>

Scrollable container for results. Caps at max-h-[300px] with scroll-py-1. Extends cmdk's Command.List.

<CommandEmpty>

Rendered when no item matches the current search. Place inside CommandList before any groups.

<CommandGroup>

Group of items with an optional heading.

PropTypeDefaultDescription
headingstring | ReactNode-Section title above the items.

Extends cmdk's Command.Group.

<CommandItem>

PropTypeDefaultDescription
valuestringtext contentInternal value used for filtering and selection.
onSelect(value: string) => void-Fired when the item is activated (Enter or click).
disabledbooleanfalseDisables interaction; renders at 50% opacity.
keywordsstring[][]Extra terms that match this item during filtering.

<CommandSeparator>

Thin bg-border divider between groups. Hidden automatically when filtering removes neighboring groups.

<CommandShortcut>

Right-aligned <span> for keyboard hints inside items (e.g. ⌘N). Pure layout helper.

<CommandDialog>

Wraps Command in a Dialog for the canonical Cmd+K palette pattern.

PropTypeDefaultDescription
titlestring"Command Palette"Screen-reader title (visually hidden).
descriptionstring"Search for a command to run..."Screen-reader description (visually hidden).
showCloseButtonbooleantrueRenders the X close button on the dialog.

All other Dialog props (open, onOpenChange, etc.) are forwarded.

Composition

  1. <Command> owns the search state and filtering logic.
  2. <CommandInput> sits at the top as the controlled search.
  3. <CommandList> holds the results.
  4. Inside the list: CommandEmpty (no-results state) followed by one or more CommandGroup blocks. CommandSeparator divides groups.
  5. Items: each CommandItem exposes onSelect; add CommandShortcut for keyboard hints.

For the floating palette pattern, wrap everything in CommandDialog and toggle its open prop on Cmd+K.

Variations

Groups with shortcuts

An inline palette grouped by intent, with right-aligned shortcut hints and a CommandEmpty state when the search returns nothing.

'use client';import {  ArchiveIcon,  FileTextIcon,  InboxIcon,  SettingsIcon,  UserIcon,} from 'lucide-react';import {  Command,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,  CommandSeparator,  CommandShortcut,} from '@gremorie/rx-overlays';export function CommandGroupsPreview() {  return (    <Command className="max-w-md rounded-lg border">      <CommandInput placeholder="Type a command or search..." />      <CommandList>        <CommandEmpty>No results found.</CommandEmpty>        <CommandGroup heading="Navigate">          <CommandItem>            <InboxIcon />            Inbox            <CommandShortcut>G I</CommandShortcut>          </CommandItem>          <CommandItem>            <FileTextIcon />            Documents            <CommandShortcut>G D</CommandShortcut>          </CommandItem>          <CommandItem>            <ArchiveIcon />            Archive            <CommandShortcut>G A</CommandShortcut>          </CommandItem>        </CommandGroup>        <CommandSeparator />        <CommandGroup heading="Account">          <CommandItem>            <UserIcon />            Profile          </CommandItem>          <CommandItem>            <SettingsIcon />            Settings            <CommandShortcut>⌘,</CommandShortcut>          </CommandItem>        </CommandGroup>      </CommandList>    </Command>  );}

Cmd+K dialog

The canonical floating launcher. The trigger below opens CommandDialog; Cmd+K (or Ctrl+K) toggles it too.

Command Palette

Search for a command to run...

'use client';import {  CalculatorIcon,  CalendarIcon,  CreditCardIcon,  SettingsIcon,  SmileIcon,  UserIcon,} from 'lucide-react';import { useEffect, useState } from 'react';import { Button } from '@gremorie/rx-forms';import {  CommandDialog,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,  CommandSeparator,  CommandShortcut,} from '@gremorie/rx-overlays';export function CommandDialogPreview() {  const [open, setOpen] = useState(false);  useEffect(() => {    const onKey = (event: KeyboardEvent) => {      if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {        event.preventDefault();        setOpen((value) => !value);      }    };    document.addEventListener('keydown', onKey);    return () => document.removeEventListener('keydown', onKey);  }, []);  return (    <>      <Button variant="outline" onClick={() => setOpen(true)}>        Open command palette        <CommandShortcut>⌘K</CommandShortcut>      </Button>      <CommandDialog open={open} onOpenChange={setOpen}>        <CommandInput placeholder="Type a command or search..." />        <CommandList>          <CommandEmpty>No results found.</CommandEmpty>          <CommandGroup heading="Suggestions">            <CommandItem onSelect={() => setOpen(false)}>              <CalendarIcon />              Calendar            </CommandItem>            <CommandItem onSelect={() => setOpen(false)}>              <SmileIcon />              Search emoji            </CommandItem>            <CommandItem onSelect={() => setOpen(false)}>              <CalculatorIcon />              Calculator            </CommandItem>          </CommandGroup>          <CommandSeparator />          <CommandGroup heading="Settings">            <CommandItem onSelect={() => setOpen(false)}>              <UserIcon />              Profile              <CommandShortcut>⌘P</CommandShortcut>            </CommandItem>            <CommandItem onSelect={() => setOpen(false)}>              <CreditCardIcon />              Billing              <CommandShortcut>⌘B</CommandShortcut>            </CommandItem>            <CommandItem onSelect={() => setOpen(false)}>              <SettingsIcon />              Settings              <CommandShortcut>⌘S</CommandShortcut>            </CommandItem>          </CommandGroup>        </CommandList>      </CommandDialog>    </>  );}

The wiring in code:

import { useEffect, useState } from 'react';

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

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

  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandInput placeholder="Search docs, run commands..." />
      <CommandList>
        <CommandEmpty>No results.</CommandEmpty>
        <CommandGroup heading="Navigate">
          <CommandItem onSelect={() => goto('/inbox')}>
            Inbox
            <CommandShortcut>G I</CommandShortcut>
          </CommandItem>
          <CommandItem onSelect={() => goto('/projects')}>
            Projects
            <CommandShortcut>G P</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </CommandDialog>
  );
}

Combobox pattern

Combine Command with Popover for an inline searchable picker.

<Popover open={open} onOpenChange={setOpen}>
  <PopoverTrigger asChild>
    <Button variant="outline" role="combobox" aria-expanded={open}>
      {value || 'Select framework...'}
    </Button>
  </PopoverTrigger>
  <PopoverContent className="w-[240px] p-0">
    <Command>
      <CommandInput placeholder="Search framework..." />
      <CommandList>
        <CommandEmpty>No framework found.</CommandEmpty>
        <CommandGroup>
          {frameworks.map((f) => (
            <CommandItem key={f.value} value={f.value} onSelect={onSelect}>
              {f.label}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  </PopoverContent>
</Popover>

Disable client filtering and drive the list from a query.

<Command shouldFilter={false}>
  <CommandInput
    value={query}
    onValueChange={setQuery}
    placeholder="Search..."
  />
  <CommandList>
    {loading && <CommandEmpty>Loading...</CommandEmpty>}
    {!loading && results.length === 0 && (
      <CommandEmpty>No results.</CommandEmpty>
    )}
    <CommandGroup>
      {results.map((r) => (
        <CommandItem key={r.id} onSelect={() => open(r.id)}>
          {r.title}
        </CommandItem>
      ))}
    </CommandGroup>
  </CommandList>
</Command>

Accessibility

  • Role: ARIA combobox pattern - input is role="combobox", list is role="listbox", items are role="option".
  • Keyboard: / move between items; Enter activates the selected item; type to filter; Esc closes when inside CommandDialog.
  • Focus management: focus is held in the input; list selection is announced via aria-selected (not by moving focus).
  • CommandDialog inherits all Dialog accessibility (focus trap, modal, return-focus-on-close).
  • Title and description: for CommandDialog, both are visually hidden but available to screen readers via Radix Dialog.
  • Empty state: CommandEmpty announces "No results" politely so users on assistive tech know the search returned nothing.
  • Reduced motion: dialog animations honor prefers-reduced-motion.
  • Dialog - the base modal that CommandDialog wraps.
  • Popover - host for the Combobox pattern.
  • Dropdown Menu - simpler action menu without search.

On this page