Starter Docs
UIBlocks

Command palette

A Cmd+K command palette in a native dialog, made of the Rabtx components. Fuzzy search, groups, recent items, nested pages, loading and empty states. Every animation is CSS.

Command palette

DarkChange theme
Go to Home
Open inbox3
Your profile
New file
New projectNew
Archive projectNeeds the owner role
Change theme
Switch teamLoads from your API
Settings
Sign out

Ran: nothing yet

Press ⌘ J (or Ctrl J) anywhere on the page, or the button. The block's default is ⌘K; this page uses shortcut="j" because ⌘K opens the docs search. Try "nf", "dark", Change theme (a page) and Switch team (a page that loads).

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/command-block

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/command-block.json

The block lands in components/blocks and the components it uses in your components/ui, styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

Usage

Installed from the registry, import from @/components/blocks/command/command-block; inside this monorepo, from @rabtx/ui/blocks/command.

import { CommandBlock, type CommandItem } from "@rabtx/ui/blocks/command";

const commands: CommandItem[] = [
  { id: "home", label: "Go to Home", group: "Navigation", shortcut: ["G", "H"], onSelect: () => router.push("/") },
  { id: "new", label: "New project", group: "Actions", badge: "New", onSelect: createProject },
  {
    id: "theme",
    label: "Change theme",
    group: "Preferences",
    placeholder: "Choose a theme…",
    items: [
      { id: "light", label: "Light", onSelect: () => setTheme("light") },
      { id: "dark", label: "Dark", onSelect: () => setTheme("dark") },
    ],
  },
  { id: "team", label: "Switch team", group: "Preferences", items: () => fetchTeams() },
];

export function AppCommands() {
  return <CommandBlock commands={commands} onSelect={(item) => track("command", item.id)} />;
}

The block is data: each command is an object.

  • Run. Enter or a click runs the command's onSelect, then the block's onSelect (with the pages above it), adds it to the recent list and closes the palette. keepOpen keeps it open.
  • Pages. A command with items opens a page of its own. The pages show as badges over the field with a Back button; Backspace in an empty field, or Escape, steps back. When items is a function, the page shows skeleton rows while it loads, and a Try again button if it fails.
  • Search. Typing filters with a fuzzy match on the label (best), then keywords, description and group. A run of letters beats scattered ones and word starts count: "nf" finds New file. On the first page a query also searches the pages below it, so "dark" finds Change theme › Dark. Matched letters are bold.
  • Recent. The last five commands run (recentLimit) are listed first while the field is empty. They are kept in memory across openings; pass recent and onRecentChange to store them.
  • Shortcut. ⌘K on Apple keyboards, Ctrl+K elsewhere; shortcut="j" changes the key and shortcut={false} binds none (do that for a second palette on one page).

Each opening starts fresh: the first page, an empty field, the first command active.

Inline, with scopes

CommandPanel is the same list without the dialog and the shortcut, for a sidebar or a page. scopes adds Tabs that narrow it to one group of the current page.

Go to Home
Open inbox3
Your profile
New file
New projectNew
Archive projectNeeds the owner role
Change theme
Switch teamLoads from your API
Settings
Sign out

Ran: nothing yet

import { CommandPanel } from "@rabtx/ui/blocks/command";

<CommandPanel commands={commands} label="Inline commands" scopes recentLimit={0} />;

For results from your API, pass them as commands with filter={false} (they are listed as given) and loading while a request runs: skeleton rows while the list is empty, a spinner in the field once results show. onQueryChange tells you what to search for.

Command palette

Type a name, such as Ada.

API

CommandBlock takes every CommandPanel prop below except surface, plus:

Prop

Type

CommandPanel:

Prop

Type

CommandItem:

Prop

Type

fuzzyMatch(text, query), commandSections(options) and pushRecent(recent, id, limit) are exported for your own lists.

Theming

The block reads only the theme tokens, through its components and command-block.css.

PartTokens
Panel--popover / --popover-foreground on the raised surface (--surface-*), --radius-box
Highlight--accent / --accent-foreground, --radius-item
Headings, hints, descriptions--muted-foreground
Key caps--border, --background, --radius-badge
Skeleton, spinner--muted, --muted-foreground
Motion--spring-unfold, --spring-panel, --spring-press, --motion-ease

How the motion works

EffectCSS
Highlightthe list's last child, placed on the active option by anchor positioning: data-active sets anchor-name, the list sets anchor-scope, and the highlight's four anchor() insets transition on the unfold spring, so it slides from row to row. Without anchor-scope the active row paints its own fill
List heightthe viewport's height follows --_h, the content's height from a ResizeObserver, on the panel spring, capped where it scrolls
Open and closethe Dialog's own: @starting-style from 8px above at 0.97, the panel spring in, a shorter fade out with display and overlay allow-discrete
Loadingskeleton rows breathe one after another; the spinner turns

Reduced motion and data-motion="off" stop the highlight and the height from moving and the skeleton from breathing; the spinner slows. Every state still shows.

Accessibility

  • The field is a combobox with aria-expanded, aria-autocomplete="list" and aria-controls pointing at the listbox. Focus stays in the field; aria-activedescendant names the active option, which has aria-selected.
  • Up and Down move (and wrap), Home and End jump, Enter runs or opens. Disabled options have aria-disabled and are skipped. The field's description says so.
  • Groups are groups named by their headings. A polite live region says how many results there are, "No results", "Loading…" or the page that opened.
  • The dialog is a native modal <dialog> named "Command palette": the page is inert, Escape closes it (on a page it steps back first) and focus returns to the trigger. The trigger has aria-keyshortcuts.
  • Matched letters are bold, not colored; the highlight is a fill, and the active row's text and icon take the accent foreground.

On this page