Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

Combobox

A searchable Select — typing narrows the options to those matching what has been typed, rather than only ever picking from the full list.

Usage#

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxTriggerIcon,
} from "neelam-ui";
 
<Combobox>
  <ComboboxInput placeholder="Search fruit…" />
  <ComboboxContent>
    <ComboboxItem value="apple">Apple</ComboboxItem>
    <ComboboxItem value="banana">Banana</ComboboxItem>
    <ComboboxEmpty>No fruit found.</ComboboxEmpty>
  </ComboboxContent>
</Combobox>

ComboboxEmpty renders only when filtering has excluded everything. Include it — without it, a search with no matches collapses to an empty box that gives the user no feedback at all.

Labelling#

ComboboxInput is a text input and needs a label like any other. Wrapping it in a <label> is the most robust option, since it survives the input being re-parented:

<label className="flex w-64 flex-col gap-1.5 text-sm font-medium">
  Fruit
  <Combobox>…</Combobox>
</label>

Placeholder is not a label

A placeholder disappears the moment the user types — exactly when they most need to remember what the field is for — and is not reliably announced. Use it for an example value, never as the field's name.

Controlled#

const [value, setValue] = useState("apple");
 
<Combobox value={value} onValueChange={setValue}>

</Combobox>

Uncontrolled use with defaultValue is supported the same way.

Combobox or Select?#

ComboboxSelect
OptionsMany; filtering earns its keep past ~10Few enough to scan
InputFree text, filters the listTypeahead jumps to a match
Best forCountries, users, repositoriesStatus, role, sort order

If the list fits on screen without scrolling, Select is the lighter and more predictable choice.

Keyboard#

Keyboard shortcuts
KeyBehaviour
Opens the list, or moves to the next option.
Moves to the previous option.
EnterSelects the active option and closes the list.
EscapeCloses the list, leaving the value unchanged.
A–ZTyping filters the list; the first match becomes active.

Accessibility#

Implements the APG combobox pattern:

  • The input carries role="combobox" with aria-expanded and aria-controls pointing at the listbox.
  • The active option is tracked with aria-activedescendant, so focus stays in the text input while the arrow keys move the selection — a screen reader user can keep typing without losing their place.
  • Options carry role="option" with aria-selected.
  • The list closes on Escape without committing a value, so an accidental open is always recoverable.

Why aria-activedescendant rather than roving focus

Moving real DOM focus into the list would take it out of the text input, and the user could no longer type to refine the search. aria-activedescendant is what the pattern specifies for exactly this reason.

API reference#

Combobox#

Props for Combobox
PropTypeDefault
childrenReactNode
defaultValuestring
disabledboolean
onValueChange((value: string) => void)
valuestring

ComboboxInput#

Props for ComboboxInput
PropTypeDefault
defaultValuestring
disabledboolean
onValueChange((value: string) => void)
valuestring

ComboboxItem#

Props for ComboboxItem
PropTypeDefault
childrenrequired

The visible label — a plain string, not arbitrary `children` the way `SelectItem` allows. Filtering has to know an item's text *before* deciding whether to render it at all, which only works against a string known up front, not something derived from rendered JSX after the fact. See `DECISIONS.md`.

string
valuerequiredstring