Skip to content

Combobox / autocomplete

In review
Live · web

Single-select: type to filter, ↑↓ to move, Enter to pick

Leading status mark · selected row gets a cyan check.

Grouped options: leading + secondary text under a header

Cloud and local engines, bucketed.

Multi-select: removable tokens (Backspace pops the last)

TypeScriptJetpack Compose

Selected values live as chips inside the field.

Async + disabled

Inline spinner; the list shows a Searching… hint.

Inert, but still legible.

Committed 0 selections. The island is live.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,
// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
// The combobox = a recessed field WELL + a lifted popover listbox you type to filter.
const PROJECTS: ComboOption[] = [
{ value: 'api-server', label: 'api-server', secondary: 'API · service layer', status: 'success' },
{ value: 'credential-store', label: 'credential-store', secondary: 'Credential store', status: 'warning' },
];
export function ProjectSwitcher() {
const [project, setProject] = useState('api-server');
const [tags, setTags] = useState<string[]>(['typescript']);
return (
<>
{/* Single-select: type to filter, ↑↓ to move, Enter to pick, Esc to close. */}
<Combobox
label="Project"
options={PROJECTS}
value={project}
onSelect={setProject}
placeholder="Search projects…"
/>
{/* Multi-select: chosen values live as removable tokens; Backspace pops the last. */}
<Combobox
label="Stack tags"
options={TAGS}
value={tags}
multiple
onSelect={(v) => setTags((t) => [...t, v])}
onRemove={(v) => setTags((t) => t.filter((x) => x !== v))}
/>
</>
);
}
PropTypeDefaultDescription
label *stringPersistent label above the well, never a placeholder. Bound to the field via htmlFor.
options *ComboOption[]Full option set; filtered live by what is typed (case-insensitive substring of label / secondary).
value *string | string[]Single selected value, or the array of selected values in multi-select.
onSelect *(value: string) => voidCommit a selection (the option value). In multi-select, toggles the value in/out.
multiplebooleanfalseRender selected values as removable tokens inside the well; Backspace on an empty input pops the last.
onRemove(value: string) => voidRemove a token (multi-select). Fired by its ✕ or by Backspace.
placeholderstringGhost prompt inside the well: supplementary, never the label.
helperTextstringQuiet helper line under the well.
disabledbooleanfalseInert + clearly seen (never faded to invisible).
loadingbooleanfalseInline spinner in the well + a Searching… hint in the list (async source).
groupedbooleanfalseBucket options under their group header.
ComboOptionTypeDefaultDescription
value *stringStable identity + the selected-value carrier.
label *stringPrimary line: the option’s accessible name (label.large role). The matched run is highlighted with a <mark>.
secondarystringSupporting line under the label (body.small role).
status'success' | 'warning' | 'error' | 'info' | 'idle'Leading status mark color. Status colors only, never the brand cyan (cyan is reserved for the selected check).
groupstringBucket name; options sharing a group render under one header when grouped.
disabledbooleanfalseUnselectable + skipped by the keyboard cursor, but still legible.