Combobox
Type to filter a list and pick from it: assignees, timezones, countries, or labels of your own.
Installation
You can add the combobox using our CLI or manually:
Dark mode ready - this component follows BoardUI semantic tokens automatically. Set up theme switching.
The CLI copies the component source (with its internal dependencies) into your project and installs any required npm packages.
npx boardui@latest add comboboxUsing a shared UI package or custom import aliases? Configure your installation paths before running the CLI. Copy-paste examples use the default imports.
Anatomy
The field is Input's own, with the same surface and focus ring, and a toggle at its end. The list opens under the whole field at its width, uses the Select menu surface, and marks the chosen row with a check. Pass defaultItems with a render function, or write ComboBoxItem rows by hand.
Every row needs a textValue when its content is more than plain text: it is what the filter matches and what the field shows once the row is chosen.
Sections
Group rows with ComboBoxSection. Set menuTrigger="focus" to open the full list when the field is focused, before anything is typed. Long lists scroll under a soft blur at their top edge.
<ComboBox label="Timezone" placeholder="Search cities" leadingIcon={RiTimeLine} menuTrigger="focus"> {timezones.map((group) => ( <ComboBoxSection key={group.title} id={group.title} title={group.title}> {group.zones.map((zone) => ( <ComboBoxItem key={zone.id} id={zone.id} textValue={zone.label}> <span className="flex-1 truncate">{zone.label}</span> <span className="text-body-2-regular text-text-tertiary">{zone.offset}</span> </ComboBoxItem> ))} </ComboBoxSection> ))}</ComboBox>Custom values
With allowsCustomValue the field works as an autocomplete: suggestions help, but any text can be kept. Read it with inputValue and onInputChange.
const [value, setValue] = useState("");
<ComboBox label="Label" placeholder="Pick or type a label" allowsCustomValue inputValue={value} onInputChange={setValue} hint="Press Enter to keep a label that is not in the list."> {labels.map((label) => <ComboBoxItem key={label} id={label}>{label}</ComboBoxItem>)}</ComboBox>Controlled
Keep the choice in your own state with selectedKey and onSelectionChange. It is null until a row is picked.
const [selected, setSelected] = useState<Key | null>("leo");
<ComboBox label="Reviewer" placeholder="Search people" defaultItems={people} selectedKey={selected} onSelectionChange={setSelected}> {(person) => <ComboBoxItem id={person.id} textValue={person.name}>{person.name}</ComboBoxItem>}</ComboBox>Sizes and states
size="small" matches the compact Input. Invalid and disabled fields take Input's colours, and emptyMessage replaces the message shown when nothing matches.
<ComboBox label="Small" size="small" placeholder="Search" defaultItems={people}>…</ComboBox><ComboBox label="Invalid" isInvalid hint="Pick someone on the team." defaultItems={people}>…</ComboBox><ComboBox label="Disabled" isDisabled defaultItems={people}>…</ComboBox>
// Shown when nothing matches<ComboBox emptyMessage="No one by that name" …>…</ComboBox>Right-to-left
Wrap the field in DirectionProvider. The icon and toggle swap sides, the list opens from the right edge, and its rows mirror.
import { DirectionProvider } from "@/components/foundations/direction/direction";
<DirectionProvider locale="ar"> <ComboBox label="الدولة" placeholder="ابحث عن دولة" defaultItems={countries} emptyMessage="لا توجد نتائج"> {(country) => <ComboBoxItem id={country.id} textValue={country.name}>{country.name}</ComboBoxItem>} </ComboBox></DirectionProvider>Keyboard and accessibility
Typing filters the list. Down and Up open it and move through the rows, Enter picks the highlighted row, and Escape closes the list, or clears the text when it is already closed. Focus stays in the field the whole time, and screen readers announce the highlighted row and the number of results.