caliper uidocs

Command palette

Free

A searchable list of destinations and actions opened from the keyboard.

Installs components/ui/command-palette.tsxDepends on @radix-ui/react-dialog, @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/command-palette.json

Press the button, then try “inv”, “#ostra”, “@ra” or “>dark”.

When to use #

Use it for

  • Apps with many places and commands.
  • Power users who prefer typing to clicking.

Skip it when

  • The app has five screens; plain navigation is faster.

Decisions #

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

Escape steps back before it closes
A typed prefix becomes a scope chip; Backspace or Escape removes the scope first, and only then does Escape close the palette.
The preview gives way on small screens
The preview pane for the highlighted item hides under 768px, so the list keeps its full width on phones.

Usage

Offer a way out when nothing matches
Pass an emptyAction, such as creating the thing that was searched for, so an empty result is a next step rather than a dead end.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/command-palette.json

Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { FileText, FolderOpen, Home, Inbox, Search as SearchIcon, Settings } from "lucide-react"
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { CommandPalette } from "@/components/ui/command-palette"
import { Search } from "@/components/ui/search"

export function CommandDemo() {
  const [open, setOpen] = useState(false);
  const [result, setResult] = useState("Press the button, then try “inv”, “#ostra”, “@ra” or “>dark”.");
  const trigger = useRef<HTMLButtonElement>(null);
  const run = (text: string) => () => setResult(text);
  const items = [
    { id: "billing", label: "Billing", group: "Go to", meta: "G B", icon: <Home aria-hidden="true" />, preview: { kind: "Page", title: "Billing", rows: [["Plan", "Team · 14 seats"], ["Next bill", "$1,284.00 · 1 Nov"]] as [string, string][], hint: "Opens Settings → Billing." }, onSelect: run("Opened Billing.") },
    { id: "inv-0417", label: "INV-0417 · Halden Analytics", group: "Invoices", meta: "$4,280.00", icon: <FileText aria-hidden="true" />, preview: { kind: "Invoice", title: "INV-0417", rows: [["Customer", "Halden Analytics"], ["Amount", "$4,280.00"], ["Status", "Paid 2 Oct"]] as [string, string][], hint: "Opens the invoice." }, onSelect: run("Opened INV-0417.") },
    { id: "inv-0416", label: "INV-0416 · Ostra Freight", group: "Invoices", meta: "$12,960.50", icon: <FileText aria-hidden="true" />, preview: { kind: "Invoice", title: "INV-0416", rows: [["Customer", "Ostra Freight"], ["Amount", "$12,960.50"], ["Status", "Due 14 Oct"]] as [string, string][], hint: "Reminder set for 11 Oct." }, onSelect: run("Opened INV-0416.") },
    { id: "priya", label: "Priya Raman", group: "People", meta: "Admin", icon: <Inbox aria-hidden="true" />, preview: { kind: "Person", title: "Priya Raman", rows: [["Role", "Admin"], ["Local time", "14:32 Lisbon"]] as [string, string][], hint: "Opens the profile." }, onSelect: run("Opened Priya Raman.") },
    { id: "rafael", label: "Rafael Abreu", group: "People", meta: "Finance", icon: <Inbox aria-hidden="true" />, preview: { kind: "Person", title: "Rafael Abreu", rows: [["Role", "Finance"], ["Local time", "10:32 São Paulo"]] as [string, string][] }, onSelect: run("Opened Rafael Abreu.") },
    { id: "create", label: "Create invoice", group: "Actions", meta: "⌘I", icon: <FolderOpen aria-hidden="true" />, preview: { kind: "Action", title: "Create invoice", rows: [["Opens", "New invoice draft"], ["Numbering", "INV-0418 next"]] as [string, string][], hint: "Nothing is sent until you choose Send." }, onSelect: run("Started INV-0418.") },
    { id: "dark", label: "Switch to dark mode", group: "Actions", meta: "⌘⇧L", icon: <Settings aria-hidden="true" />, keywords: ["theme"], preview: { kind: "Action", title: "Switch to dark mode", rows: [["Now", "Light"], ["After", "Dark, saved on this device"]] as [string, string][] }, onSelect: run("Switched to dark mode.") },
  ];
  return (
    <div className="grid justify-items-center gap-3">
      <Button ref={trigger} variant="secondary" onClick={() => setOpen(true)}><SearchIcon aria-hidden="true" />Search or run a command</Button>
      <p role="status" className="max-w-sm text-center text-sm text-muted-foreground">{result}</p>
      <CommandPalette
        open={open}
        onOpenChange={setOpen}
        items={items}
        scopes={[{ prefix: ">", label: "Actions", group: "Actions" }, { prefix: "@", label: "People", group: "People" }, { prefix: "#", label: "Invoices", group: "Invoices" }]}
        emptyAction={{ label: (query) => `Create invoice for “${query}”`, onSelect: (query) => { setOpen(false); setResult(`Started a draft invoice for “${query}”.`); } }}
        onCloseAutoFocus={(event) => { event.preventDefault(); trigger.current?.focus(); }}
      />
    </div>
  );
}

API reference #

CommandPalette

component
PropTypeDefaultDescription
open *boolean—
onOpenChange *(open: boolean) => void—
items *readonly CommandItem[]—
titlestring"Command palette"
descriptionstring"Search destinations, projects, and actions."
classNamestring—
onCloseAutoFocus((event: Event) => void)—
scopesreadonly CommandScope[][]
emptyAction{ label: (query: string) => string; onSelect: (query: string) => void; }—Offered when nothing matches, e.g. create an invoice for the typed name.
placeholderstring"Find a project or action…"The search field's prompt, naming what can be found, e.g. "Search people, companies and deals…".

Accessibility #

It exposes combobox, listbox, group, option and status semantics, with aria-hidden, aria-autocomplete, aria-expanded, aria-controls, aria-activedescendant and aria-label kept in step with state. Changes that matter are announced through a live region.

KeyBehaviour
BackspaceRemoves the previous character or item.
EnterActivates the focused control.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.
↓Moves to the next item or value.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.