VIII Wayfinding

Star on GitHubFeedback
VIII Wayfindingcmdkot-command

Command

What you type is set as large as a headline, in italic. Matches are marked; the chosen row steps forward. Or every row set on one axis at its match, as a mesostic; or the groups run on, as a book's index.

Report an issue with Command

Example

Mesostic
Index
Headline
Headline, matching “ar”
Mesostic, “a”
Index, “o”
Index, nothing matches

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and scrollbar along.

npx shadcn@latest add https://thedirectors.agency/ui/r/command.json

Usage

This example also uses Button and Kbd.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json https://thedirectors.agency/ui/r/kbd.json
command-example.tsx
"use client"import * as React from "react"import { Button } from "@/components/ui/button"import {Command,CommandDialog,CommandEmpty,CommandGroup,CommandHint,CommandInput,CommandItem,CommandList,CommandShortcut,} from "@/components/ui/command"import { Kbd } from "@/components/ui/kbd"import { State, Still } from "@/components/site/state"const PROJECTS = [["Halden", "Identity, 2026"],["Northlight", "Web, 2026"],["Oda Studio", "Identity, 2025"],["Tidewater", "Motion, 2025"],["Marram", "Web, 2025"],]function Palette({ onRun, whole }: { onRun: (said: string) => void; whole?: boolean }) {return (<><CommandInput placeholder="Type to search" /><CommandList style={whole ? { maxHeight: "none" } : undefined}><CommandEmpty>Nothing matches. Try a project, like Halden.</CommandEmpty><CommandGroup heading="Projects">{PROJECTS.map(([name, hint]) => (<CommandItem key={name} value={name} keywords={[hint]} onSelect={() => onRun(`Opened ${name}.`)}>{name}<CommandHint>{hint}</CommandHint></CommandItem>))}</CommandGroup><CommandGroup heading="Actions"><CommandItem value="Start a project" onSelect={() => onRun("Started a new project.")}>Start a project</CommandItem><CommandItem value="Show the grid" keywords={["layout"]} onSelect={() => onRun("Showed the grid.")}>Show the grid<CommandShortcut>G</CommandShortcut></CommandItem></CommandGroup></CommandList></>)}/** The palette opens from the button or from ⌘K / Ctrl+K; the key is registered here, not in the item. */export default function Example() {const [open, setOpen] = React.useState(false)const [said, setSaid] = React.useState("")const still = React.useContext(Still)React.useEffect(() => {if (still) returnconst down = (e: KeyboardEvent) => {if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {e.preventDefault()setOpen((o) => !o)}}document.addEventListener("keydown", down)return () => document.removeEventListener("keydown", down)}, [still])return (<div className="grid w-full max-w-[40rem] justify-items-stretch" style={{ gap: "var(--ot-space-8)" }}><div className="flex flex-wrap items-center" style={{ gap: "var(--ot-space-5)" }}><Button variant="bracket" onClick={() => setOpen(true)}>Search <Kbd>⌘K</Kbd></Button><p aria-live="polite">{said}</p></div><div className="grid" style={{ gap: "var(--ot-space-3)" }}><span className="ot-label">Mesostic</span><Command variant="mesostic" aria-label="Projects and actions, as a mesostic" defaultSearch="ar"><Palette onRun={setSaid} whole /></Command></div><div className="grid" style={{ gap: "var(--ot-space-3)" }}><span className="ot-label">Index</span><Command variant="index" aria-label="Projects and actions, as an index"><Palette onRun={setSaid} whole /></Command></div><CommandDialog open={open} onOpenChange={setOpen} title="Search projects and actions"><PaletteonRun={(next) => {setSaid(next)setOpen(false)}}/></CommandDialog></div>)}

Contract

  • Underneath: cmdk; CommandDialog puts it in a Radix Dialog.
  • Anatomy: .ot-command[data-variant="headline | mesostic | index"] holding <input class="ot-command-input" role="combobox">, .ot-command-list (role="listbox") of .ot-command-group headings and .ot-command-option rows (.ot-command-name, split at the first match into .ot-command-before and .ot-command-key; .ot-command-hint; the arrow drawn after, with empty alt text), then .ot-command-empty. defaultSearch opens it on a query.
  • What you type is set as large as a headline, in italic. Matches are marked with the highlighter; the chosen row steps forward and shows its arrow. Rows arrive in turn as the list changes.
  • mesostic: John Cage's mesostics and the printed concordance (key word in context). Every row is set on one vertical axis at the place your words appear: the part before the match set flush against the axis from the left, the match and the rest from the right. The found letters stand on the axis in roman capitals at 600 in ink, with no highlighter, so the query reads straight down through the results; later matches in the same row aren't marked. The query itself is typed on the same axis above them, and the group headings and empty copy start there too. The axis is the middle of the rows less the arrow's column (space-6, space-5 on a phone). Hints stand at the start of the row, as a concordance's locators do; on a phone they drop under the key. Nothing steps forward (that would break the axis): the chosen line inks and its arrow shows. In RTL the columns mirror, so the text before the match sits right of the axis, as it reads.
  • index: a book's index, run in. Each group's name hangs in the margin (7rem) as the headword, and its rows run on after it as one paragraph at mp, sub-entries parted by semicolons and closed with a full stop, hints small after each name as locators. Matches are marked with the highlighter; the chosen entry is underlined with a hairline in ink. Rows that stop matching leave the sentence, which closes up; the entries fade in in turn. Below 40rem the headword sits over its paragraph.
  • Keyboard: Down and Up move, Enter runs, in every variant (the index moves along the sentence in reading order). Empty copy names something to try.
  • The chosen row is brought into view only while you're in the palette, so mounting one never scrolls the page.
  • CommandDialog: the ⌘K palette; variant and defaultSearch pass through. A Radix Dialog holds the command over a dimmed page, with a visually hidden title. The page registers the shortcut; the item doesn't.

Props

Props of Command
PropTypeDefaultWhat it does
variant"headline" | "mesostic" | "index""headline"On Command or CommandDialog. headline: your query set large, the rows under it. mesostic: every row aligned on one axis at the place your words appear, the found letters in roman capitals, your query on the same axis above. index: each group's name in the margin, its rows run on after it as one paragraph, parted by semicolons.
defaultSearchstring""On Command or CommandDialog. What the input holds when it opens, for a palette that starts from a query.
CommandDialog open / onOpenChangeboolean / (open) => voidThe ⌘K palette as a modal (Radix Dialog). It opens where you tell it; register ⌘K / Ctrl+K yourself.
CommandDialog onCloseAutoFocus(event: Event) => voidWhere focus goes when the palette closes. By default it returns to what had focus when it opened, which is the page itself if a key opened it; preventDefault and focus your own trigger instead.
CommandDialog titlestring"Search"Read out by screen readers; not shown.
CommandItem value / keywords / onSelectstring / string[] / () => voidWhat the row is called, other words that find it, and what Enter does. A row matches when what you typed sits inside its value or keywords, and the words in it are marked.
CommandGroup headingReactNodeA small heading over a group of rows.
CommandEmptyReactNodeShown when nothing matches. Name something to try.
CommandShortcutkbdA key that runs the row, drawn as the corners of a cap.
CommandHintspanA quiet word at the end of a row that isn't a key: a year, a kind.
filter(value, search, keywords) => numberOn Command. Replaces the substring match; the highlighter still marks what you typed.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Combobox, menu, commandWeingart letterThe highlighter marks what matches
Command"the uncreative"What you type, set as a headline
Mesostic commandJohn Cage's mesostics; the printed concordanceEvery row set on one axis at its match, your query on the axis above
Index commandA book's run-in indexEach group a headword with its rows run on as a sentence

How it moves

How it moves
ComponentArticulationWhat moves
CommandArpeggioThe rows arrive; the chosen one steps forward
Mesostic commandInkThe rows arrive on the axis; the chosen line inks and its arrow draws in
Index commandArpeggioThe entries fade into the sentence in turn; the chosen one's underline inks