Combobox
A field you can type into and also choose from. The text filters the list — and, unless you say otherwise, can become the value itself.
import { Combobox } from 'neba';
<Combobox
label="Framework"
placeholder="Search or type your own"
items={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' }
]}
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. The same shell as a TextField, so the two are indistinguishable in a form |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale. With multiple it becomes a minimum, because the field grows as the chips wrap |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. The surface is white, so it reaches the edge, the focus ring and the chips |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth of the field. The popup carries its own |
| items * | readonly ComboboxOption[] | — | The options, as an array of { value, label?, disabled? } — label is a string, not a ReactNode |
| multiple | boolean | false | Whether more than one value may be held. The chosen ones become chips inside the field |
| value | string | number | (string | number)[] | null | — | The chosen value — an array when multiple. Use with onValueChange for a controlled combobox |
| defaultValue | string | number | (string | number)[] | null | — | The initial value, for an uncontrolled combobox |
| onValueChange | (value) => void | — | Called when the chosen value changes |
| onInputValueChange | (inputValue: string) => void | — | Called as the text in the input changes — the filter query, not the value |
| allowCustom | boolean | true | Whether a value the list does not contain may be committed. The typed text is offered as its own row at the end of the list — what separates this from a searchable select |
| customLabel | (query: string) => ReactNode | Add “…” | What that row says |
| clearable | boolean | false | Shows a × that empties the field. A field that can be cleared in one click can be emptied by accident |
| emptyMessage | ReactNode | 'No matches' | Shown in the popup when nothing matches and no value may be added |
| limit | number | -1 | The most rows the list will show at once. -1 is all of them |
| placeholder | string | — | Shown in the input while nothing is typed |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| startIcon | ReactNode | — | Content before the input |
| fullWidth | boolean | false | Stretches to the width of the container |
| removeLabel | (label: string) => string | Remove … | Accessible name of a chip's remove button. Receives the chip's label |
| clearLabel | string | 'Clear' | Accessible name of the clear button |
| name | string | — | Identifies the field when a form is submitted |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Options are data
The same shape Select takes, with one difference:
interface ComboboxOption {
value: string | number;
label?: string; // a string, not a ReactNode
disabled?: boolean;
}label is a string here because the filter types against it and the input writes it out, and neither of those can be done to an element. Values stay strings and numbers — a combobox is a form control and its value is what gets submitted.
Examples
Multiple
The chosen values become Chips inside the field, and the input goes on filtering after each one, so a set of tags is built without the field ever closing. Backspace on an empty input reaches back for the last chip.
A value the list does not have
This is what separates a combobox from a searchable select, and it is on by default.
The typed text is offered as its own row at the end of the list rather than committed quietly on blur. That is deliberate: a field that turns a half-finished word into a value the moment focus leaves is a field that invents data. Making it a row means Enter, a click and the arrow keys all reach it the way they reach every other row, and a screen reader announces it as one more option.
The first match lights up as you type, so it still takes one key: type something the list has and Enter picks it, type something it does not and Enter adds it.
Turn it off with allowCustom={false} for a field whose values are a closed set — then emptyMessage is what a fruitless search says.
Variants
The same three weights a TextField has, drawn on the same shell.
Sizes
A chip inside the field sits one step down the control ladder, and the field's padding is what is left over — so a single-row combobox is exactly as tall as the field beside it. With multiple the field grows as the chips wrap; it never has a fixed height.
States
The popup
Identical to Select's, because a combobox's list and a select's list are the same list: a floating surface at elevation 3, portalled to the end of <body>, with neba-portal on the positioner as a hook for a host that scoped its CSS reset to a subtree.
Accessibility
- Base UI owns the filtering and its collator, the popup's positioning and flipping, the
combobox/listboxwiring, arrow-key navigation across both the list and the chips, and the hidden input that makes the field submit with a form. labelbecomes the accessible name.- A disabled option stays in the list and reports
aria-disabled— the option exists, it just cannot be picked. - Each chip's remove button is named through
removeLabel, which receives the chip's own label so the button says Remove documentation rather than Remove.