VI Controls

Star on GitHubFeedback
VI Controlscmdkot-combo

Combobox

A field that suggests. Type and the letters you typed are marked in each match, the suggestions arrive in turn, and what you chose sits on the line in italic; with multiple, your choices are written there as one sentence.

Report an issue with Combobox

Example

list

Type a year or a kind to narrow the list.

concordance
pencil

Tab takes the pencilled rest of the name.

multiple

Choose as many as you like. Press a name to take it out.

Rest
Hover
Focus
Chosen
Error

Choose a typeface.

Disabled
pencil, rest
pencil, focus
pencil, chosen
multiple, rest
multiple, focus
multiple, chosen
multiple, disabled
CaslonGill Sans

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, badge, command, field, and popover along.

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

Usage

combobox-example.tsx
"use client"import * as React from "react"import { Combobox } from "@/components/ui/combobox"import { Field } from "@/components/ui/field"const FACES = ["Akzidenz-Grotesk", "Archivo", "Avenir", "Baskerville", "Bodoni Moda", "Bricolage Grotesque", "Caslon", "Cormorant", "Didot", "EB Garamond", "Frutiger", "Futura", "Gill Sans", "Helvetica", "Instrument Sans", "Newsreader", "Schibsted Grotesk", "Univers"].map((name) => ({value: name.toLowerCase().replace(/\s+/g, "-"),label: name,}))const PROJECTS = [{ value: "halden", label: "Halden", hint: "Identity, 2026" },{ value: "northlight", label: "Northlight", hint: "Web, 2026" },{ value: "oda-studio", label: "Oda Studio", hint: "Identity, 2025" },{ value: "tidewater", label: "Tidewater", hint: "Motion, 2025" },{ value: "marram", label: "Marram", hint: "Web, 2025" },]export default function Example() {const [project, setProject] = React.useState("")return (<div className="grid w-full max-w-md gap-12"><div className="grid gap-10"><span className="ot-label">list</span><Combobox label="Typeface" placeholder="Start typing a name" options={FACES} empty="Nothing matches. Try part of a name, like Garamond." /><Field label="Project" hint="Type a year or a kind to narrow the list."><Combobox options={PROJECTS} placeholder="Choose a project" value={project} onValueChange={setProject} /></Field></div><div className="grid gap-10"><span className="ot-label">concordance</span><Combobox variant="concordance" label="Typeface" placeholder="Type any part of a name" options={FACES} empty="Nothing matches. Try a few letters, like ond." /></div><div className="grid gap-10"><span className="ot-label">pencil</span><Field label="Typeface" hint="Tab takes the pencilled rest of the name."><Combobox variant="pencil" placeholder="Start typing a name" options={FACES} empty="Nothing matches. Try part of a name, like Garamond." /></Field></div><div className="grid gap-10"><span className="ot-label">multiple</span><Field label="Typefaces" hint="Choose as many as you like. Press a name to take it out."><Combobox multiple name="faces" placeholder="Choose any number" options={FACES} defaultValue={["didot", "futura", "univers"]} empty="Nothing matches. Try part of a name, like Garamond." /></Field></div></div>)}

Contract

  • Underneath: cmdk inside a Radix Popover.
  • Anatomy: .ot-field.ot-combo-field holding the label and the trigger, <button class="ot-combo" role="combobox" aria-haspopup="listbox" aria-expanded aria-controls>, whose .ot-combo-value shows the chosen label, or the placeholder with data-placeholder. It opens .ot-combo-pop (the popover) holding a ot-command: its input to narrow the list, then .ot-command-list (role="listbox") of .ot-command-option rows (role="option"); .ot-command-empty says what to try when nothing matches. Pencil has no trigger and no panel: .ot-combo-pencil holds its own <input role="combobox"> on the line, <ul class="ot-combo-list" role="listbox"> of <li role="option"> under it, and .ot-combo-empty.
  • In each suggestion, the letters you typed are wrapped in <mark> (the highlighter), and the suggestions arrive in turn. The active option carries a dot (aria-selected). The picked value is yours, in italic.
  • Keyboard: Down and Up move through the list, Enter takes one, Escape closes it.
  • Variants go on data-variant: list (the default), concordance, pencil.
  • Concordance, after the concordance's keyword-in-context column and Rand's grid: the suggestions are set so what you typed lines up in one column, highlighted, with each word's start set to the left of it and the rest running on to the right. As you type, the rows glide sideways to keep the match on that axis. A long suggestion ends in an ellipsis.
  • Pencil, after the letter-cutter's pencilled layout ("pointing sketches in pencil, choosing inks"): no panel. The rest of the best match is pencilled after your words, in place, and the few others are listed in flow under the line. Tab (or the arrow toward the end, at the end of the line) inks the pencilled rest; Down and Up change which match is pencilled; Enter takes it; Escape puts back what was there. Choosing turns the word from pencil to ink. Its own input is the combobox, with aria-activedescendant on its own listbox. Forced colours set the pencilled rest in GrayText. Pencil forwards native input props and the caller's ref to its input; caller accessibility associations take precedence over a Field's, and handlers run before internal behaviour.
  • multiple (with list or concordance): any number of choices, value an array in the order they were chosen. The line (.ot-combo-many, a PopoverAnchor) writes them as one sentence with the badge's series tags ("Didot, Futura and Univers"): yours in the italic, ours the commas and the "and". Each word is a removable tag: pressing it strikes it and the sentence closes up round the gap, focus moving to the next word, or to the line after the last. The rest of the line, after the last word, is the trigger (it reads "3 chosen" to a screen reader); the baseline runs under the whole line and takes the accent on focus. The list stays open while you choose, Enter toggles the row under the dot, and a chosen row turns into the italic instead of taking the accent (many choices, one accent); rows carry aria-checked in a list marked aria-multiselectable. With name, each chosen value is sent as its own hidden input; disabled choices submit none. The pencil stays single.

Props

Props of Combobox
PropTypeDefaultWhat it does
variant"list" | "concordance" | "pencil""list"list: the line opens a list, and the letters you typed are marked in each match. concordance: the matches hang from what you typed, lined up on it in one column, as a concordance sets a word in the middle of every line that holds it; typing moves the column and the rows glide to it. pencil: you type on the line itself, and the rest of the best match is pencilled in after the caret in our roman; Tab (or the arrow toward the end) takes it and it inks into your italic, and the first five matches sit under the line.
options{ value, label, hint? }[]The choices. Labels should be unique: they are what the list is matched and marked on. A hint is a quiet word at the end of the row, and typing it finds the row too.
value / defaultValue / onValueChangestring, (value) => voidThe chosen option's value, controlled or not. With multiple, an array of values in the order they were chosen.
multiplebooleanfalseAny number of choices, with list or concordance (the pencil stays single). What you chose is written on the line as one sentence in your italic, the badge's series tags ("Didot, Futura and Univers"); pressing a word strikes it and the sentence closes up round the gap. The rest of the line opens the list, which stays open while you choose; Enter toggles a row, and a chosen row turns italic rather than taking the accent. className goes on the line that holds the words.
namestringWith multiple, each chosen value is sent with the form as its own hidden input under this name; a disabled control submits none.
labelstringSet above the line. Inside a Field, the Field's label is used instead.
placeholderstring"Choose one"What the closed line says before anything is chosen.
emptystring"Nothing matches. Try part of a name."Shown when nothing matches. Name something to try.
KeyboardnoteEnter, Space or the down arrow opens the list. Type to narrow it, arrow keys move the dot, Enter picks. Escape closes it and puts focus back on the line. The pencil is typed into directly: Up and Down move through the matches, Enter picks, Tab takes the pencilled rest, Escape puts the line back. With multiple, Enter toggles a row and the list stays open; each chosen word on the line is its own button before the trigger, and Enter or Space takes it out.
className / id / aria-*nativePassed to the button that opens the list, or the input for pencil. Caller aria-* attributes take precedence over a Field's hint and error. Pencil accepts native input props and forwards ref to its input; handlers run before its internal behaviour. className styles the pencil wrapper.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Combobox, menu, commandWeingart letterThe highlighter marks what matches
Concordance comboboxThe concordance's keyword column; Paul Rand's gridWhat you typed stands in one column down the list
Pencil comboboxThe letter-cutter's pencilled layoutThe rest of the name pencilled after your words; Tab inks it
Multiple comboboxThe written list (serial punctuation); ours roman, yours italicThe choices written on the line as one sentence; a chosen row turns italic

How it moves

How it moves
ComponentArticulationWhat moves
ComboboxArpeggioThe suggestions arrive in turn
Concordance comboboxGlide to the axisAs you type, each row glides sideways to keep the match in its column
Pencil comboboxPencil, then inkChosen, the word inks from pencil
Multiple comboboxClose-upA word taken out is struck, then the sentence closes up and re-sets its commas