Home

Command bar

The ⌘K bar: a dialog with one search field that finds pages, records and actions in groups and runs the one picked.

ui.commandcomponents/ui/command.tsx

Use case

For the AI

Use it to move fast across a whole internal tool: jumping to a client by name, opening the Invoices page, running New Invoice or New Client from any page. It opens with ⌘K (Ctrl K) and a button in the top bar, and is keyboard first: type, the arrows, Enter.

Use it when

  • One place to jump to any page or record by name
  • Running common actions from anywhere (New Invoice, New Client)
  • A tool with more pages and records than the sidebar can show

Use something else when

Examples

⌘ K anywhere on this page, or the button

Code

Import

import { useEffect, useState } from "react"
import { FileTextIcon, SearchIcon, UsersIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandCollection,
  CommandDialog,
  CommandDialogPopup,
  CommandDialogTrigger,
  CommandEmpty,
  CommandGroup,
  CommandGroupLabel,
  CommandInput,
  CommandItem,
  CommandItemIcon,
  CommandList,
  CommandPanel,
  CommandShortcut,
} from "@/components/ui/command"

Usage

type Entry = { value: string; label: string; icon: React.ComponentType; keys: string[] }
const GROUPS: Array<{ value: string; items: Entry[] }> = [
  { value: "Pages", items: [{ value: "clients", label: "Clients", icon: UsersIcon, keys: ["G", "C"] }] },
  { value: "Actions", items: [{ value: "new-invoice", label: "New Invoice", icon: FileTextIcon, keys: ["⌘", "I"] }] },
]

export function CommandBar() {
  const [open, setOpen] = useState(false)
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen((o) => !o)
      }
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [])
  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandDialogTrigger render={<Button variant="secondary" />}>
        <SearchIcon />
        Search or Jump To
      </CommandDialogTrigger>
      <CommandDialogPopup>
        <Command items={GROUPS}>
          <CommandInput placeholder="Search clients, pages and actions…" />
          <CommandPanel>
            <CommandEmpty>Nothing matches that. Try a client&apos;s name.</CommandEmpty>
            <CommandList>
              {(group: (typeof GROUPS)[number]) => (
                <CommandGroup key={group.value} items={group.items}>
                  <CommandGroupLabel>{group.value}</CommandGroupLabel>
                  <CommandCollection>
                    {(item: Entry) => (
                      <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                        <CommandItemIcon>
                          <item.icon />
                        </CommandItemIcon>
                        <span className="truncate">{item.label}</span>
                        <CommandShortcut keys={item.keys} />
                      </CommandItem>
                    )}
                  </CommandCollection>
                </CommandGroup>
              )}
            </CommandList>
          </CommandPanel>
        </Command>
      </CommandDialogPopup>
    </CommandDialog>
  )
}

Rules

  • The page wires ⌘K itself: a `keydown` listener for K with `metaKey` or `ctrlKey` that toggles `open`; the component binds no key.
  • Each item's `onClick` runs it and closes the dialog (`setOpen(false)`).
  • Groups are `items` of `{ value, items }`: `CommandList` renders each group, `CommandCollection` its rows.
  • Actions are Title Case ("New Invoice"), with their icon in `CommandItemIcon` and their keys in `CommandShortcut`; `CommandEmpty` says what to try.
  • The bar glides to its new height as results change, its top fixed, rows leaving blurred and arriving sharpening (useGlide on the popup, built in): never size the bar by hand.