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.
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-paletteCopies 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.jsonWorks, 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
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.
| Prop | Type | Default |
|---|---|---|
labelrequiredAccessible name of the dialog, e.g. "Command palette". | string | none |
childrenrequiredCommandGroup and CommandItem elements. | node | none |
isOpenControlled open state. | boolean | none |
defaultOpenInitial state when uncontrolled. | boolean | false |
onOpenChangeCalled when it opens or closes. | (isOpen: boolean) => void | none |
placeholderHint in the search box. | string | "Search commands…" |
emptyMessageShown when nothing matches. | string | "No results found." |
shortcutLetter 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" |
onActionCalled with the id of any command run, in addition to the command's own onAction. | (key: Key) => void | none |
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