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.command
components/ui/command.tsxUse case
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
- Filtering the list on the current page: Search field
- Picking a record as a value in a form: Combobox
- The actions on one record: Dropdown menu
Examples
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'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.