Skip to content

Command Palette

A search box in a dialog that finds and runs commands, jumps to pages and triggers actions from the keyboard. Opens with Ctrl or Cmd + K. Use CommandGroup and CommandItem for the results.

Overlayv0.1.0experimentalWCAG 2.2 AAView spec

Press Ctrl K to open the palette.

Nothing run yet.

import { useState } from "react";
import { Button, CommandItem, CommandPalette, Kbd } from "@rdloom/react";

// Ctrl+K (Cmd+K on a Mac) opens and closes it from anywhere on the page.
export default function CommandPaletteBasicExample() {
  const [last, setLast] = useState("");
  return (
    <div className="flex flex-col items-center gap-3 text-sm">
      <p className="flex items-center gap-1.5">
        Press <Kbd>Ctrl</Kbd> <Kbd>K</Kbd> to open the palette.
      </p>
      <CommandPalette label="Command palette" onAction={(key) => setLast(String(key))}>
        <CommandItem id="new-project">New project</CommandItem>
        <CommandItem id="invite">Invite a teammate</CommandItem>
        <CommandItem id="settings">Open settings</CommandItem>
        <CommandItem id="theme" keywords="dark light appearance">
          Switch theme
        </CommandItem>
      </CommandPalette>
      <p aria-live="polite" className="text-[var(--rd-color-text-muted)]">
        {last ? `Ran: ${last}` : "Nothing run yet."}
      </p>
    </div>
  );
}

Installation

npx rdloom add command-palette

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/command-palette.json

Works, but without upgrade tracking.

Usage

import { Button, CommandItem, CommandPalette, Kbd } from "@rdloom/react";

<div className="flex flex-col items-center gap-3 text-sm">
  <p className="flex items-center gap-1.5">
    Press <Kbd>Ctrl</Kbd> <Kbd>K</Kbd> to open the palette.
  </p>
  <CommandPalette label="Command palette" onAction={(key) => setLast(String(key))}>
    <CommandItem id="new-project">New project</CommandItem>
    <CommandItem id="invite">Invite a teammate</CommandItem>
    <CommandItem id="settings">Open settings</CommandItem>
    <CommandItem id="theme" keywords="dark light appearance">
      Switch theme
    </CommandItem>
  </CommandPalette>
  <p aria-live="polite" className="text-[var(--rd-color-text-muted)]">
    {last ? `Ran: ${last}` : "Nothing run yet."}
  </p>
</div>

Grouped

import { useState } from "react";
import { Button, CommandGroup, CommandItem, CommandPalette } from "@rdloom/react";

const FolderIcon = () => (
  <svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
    <path d="M2.75 5.5A1.5 1.5 0 0 1 4.25 4h3.4l1.6 1.75h6.5a1.5 1.5 0 0 1 1.5 1.5v6.75a1.5 1.5 0 0 1-1.5 1.5H4.25a1.5 1.5 0 0 1-1.5-1.5V5.5z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
  </svg>
);

const GearIcon = () => (
  <svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
    <circle cx="10" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.5" />
    <path d="M10 2.5v2M10 15.5v2M2.5 10h2M15.5 10h2M4.7 4.7l1.4 1.4M13.9 13.9l1.4 1.4M4.7 15.3l1.4-1.4M13.9 6.1l1.4-1.4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
  </svg>
);

// Groups give long lists a shape; icons and shortcut hints are optional extras.
export default function CommandPaletteGroupedExample() {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <Button variant="secondary" onPress={() => setOpen(true)}>
        Open commands
      </Button>
      <CommandPalette label="Commands" isOpen={open} onOpenChange={setOpen} shortcut={null} placeholder="Type a command or search…">
        <CommandGroup title="Go to">
          <CommandItem id="projects" icon={<FolderIcon />} shortcut="G P">
            Projects
          </CommandItem>
          <CommandItem id="settings" icon={<GearIcon />} shortcut="G S">
            Settings
          </CommandItem>
        </CommandGroup>
        <CommandGroup title="Actions">
          <CommandItem id="new" shortcut="Ctrl N">
            Create a project
          </CommandItem>
          <CommandItem id="export">Export all data</CommandItem>
        </CommandGroup>
      </CommandPalette>
    </div>
  );
}

Controlled

Closed
import { useState } from "react";
import { Button, CommandItem, CommandPalette } from "@rdloom/react";

// shortcut={null} turns off Ctrl+K: you decide when it opens.
export default function CommandPaletteControlledExample() {
  const [open, setOpen] = useState(false);
  return (
    <div className="flex items-center gap-3 text-sm">
      <Button onPress={() => setOpen(true)}>Search everything</Button>
      <span aria-live="polite">{open ? "Open" : "Closed"}</span>
      <CommandPalette label="Search" isOpen={open} onOpenChange={setOpen} shortcut={null}>
        <CommandItem id="a">First command</CommandItem>
        <CommandItem id="b">Second command</CommandItem>
      </CommandPalette>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
labelrequired

Accessible name of the dialog, e.g. "Command palette".

stringnone
childrenrequired

CommandGroup and CommandItem elements.

nodenone
isOpen

Controlled open state.

booleannone
defaultOpen

Initial state when uncontrolled.

booleanfalse
onOpenChange

Called when it opens or closes.

(isOpen: boolean) => voidnone
placeholder

Hint in the search box.

string"Search commands…"
emptyMessage

Shown when nothing matches.

string"No results found."
shortcut

Letter that opens it with Ctrl (Windows, Linux) or Cmd (Mac) held. Set null to turn the shortcut off and open it yourself.

string | null"k"
onAction

Called with the id of any command run, in addition to the command's own onAction.

(key: Key) => voidnone

Accessibility

Role dialog, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Ctrl+K (Cmd+K on Mac) opens it, and closes it when open
  • Typing filters the commands as you type
  • Up and Down arrows move through the results; Home and End jump to the first and last
  • Enter runs the highlighted command and closes the palette
  • Esc closes it and returns focus to where you were

Screen readers announce

  • Opening announces the dialog name and the search box
  • Moving through results announces each command with its group and position
  • Typing announces the number of results, or that there are none
  • Running a command closes the dialog and focus returns to the page

What your code must do

  • A modal dialog with an accessible name; focus moves into the search box when it opens and is restored on close
  • The search box stays focused while arrows move a highlight through the results (an active descendant), so typing never stops working
  • Groups have visible headings that are also read to screen readers
  • When nothing matches, the empty message is announced
  • Shortcut hints on commands are display only, read after the command name
  • The backdrop blur is decoration; the dialog meets 4.5:1 contrast without it

Guidelines

Use it when

  • Apps with many actions or pages, where search is faster than menus
  • Power users who prefer the keyboard

Avoid it when

  • A short list of actions: use Menu
  • Choosing a value for a form field: use Combobox
  • The only way to reach important features

Don't

  • Hiding essential navigation behind it
  • Commands whose names are not words people would search for
  • Not telling people the shortcut exists

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-raised
  • --rd-color-surface-subtle
  • --rd-color-overlay-backdrop
  • --rd-color-border-default
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-focus-ring
  • --rd-radius-overlay
  • --rd-radius-control