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 CommandExample
Mesostic
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Index
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Headline
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Headline, matching “ar”
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Mesostic, “a”
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Index, “o”
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
Index, nothing matches
Nothing matches. Try a project, like Halden.
HaldenIdentity, 2026
NorthlightWeb, 2026
Oda StudioIdentity, 2025
TidewaterMotion, 2025
MarramWeb, 2025
Start a project
Show the gridG
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.jsonUsage
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"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;
CommandDialogputs 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-groupheadings and.ot-command-optionrows (.ot-command-name, split at the first match into.ot-command-beforeand.ot-command-key;.ot-command-hint; the arrow drawn after, with empty alt text), then.ot-command-empty.defaultSearchopens 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-5on 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 atmp, 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;variantanddefaultSearchpass 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
| Prop | Type | Default | What 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. |
defaultSearch | string | "" | On Command or CommandDialog. What the input holds when it opens, for a palette that starts from a query. |
CommandDialog open / onOpenChange | boolean / (open) => void | The ⌘K palette as a modal (Radix Dialog). It opens where you tell it; register ⌘K / Ctrl+K yourself. | |
CommandDialog onCloseAutoFocus | (event: Event) => void | Where 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 title | string | "Search" | Read out by screen readers; not shown. |
CommandItem value / keywords / onSelect | string / string[] / () => void | What 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 heading | ReactNode | A small heading over a group of rows. | |
CommandEmpty | ReactNode | Shown when nothing matches. Name something to try. | |
CommandShortcut | kbd | A key that runs the row, drawn as the corners of a cap. | |
CommandHint | span | A quiet word at the end of a row that isn't a key: a year, a kind. | |
filter | (value, search, keywords) => number | On Command. Replaces the substring match; the highlighter still marks what you typed. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Combobox, menu, command | Weingart letter | The highlighter marks what matches |
| Command | "the uncreative" | What you type, set as a headline |
| Mesostic command | John Cage's mesostics; the printed concordance | Every row set on one axis at its match, your query on the axis above |
| Index command | A book's run-in index | Each group a headword with its rows run on as a sentence |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Command | Arpeggio | The rows arrive; the chosen one steps forward |
| Mesostic command | Ink | The rows arrive on the axis; the chosen line inks and its arrow draws in |
| Index command | Arpeggio | The entries fade into the sentence in turn; the chosen one's underline inks |