Skip to content

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.

tsx
import { Combobox } from 'neba';

<Combobox
  label="Framework"
  placeholder="Search or type your own"
  items={[
    { value: 'react', label: 'React' },
    { value: 'vue', label: 'Vue' }
  ]}
/>;

Props

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop 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
multiplebooleanfalseWhether more than one value may be held. The chosen ones become chips inside the field
valuestring | number | (string | number)[] | nullThe chosen value — an array when multiple. Use with onValueChange for a controlled combobox
defaultValuestring | number | (string | number)[] | nullThe initial value, for an uncontrolled combobox
onValueChange(value) => voidCalled when the chosen value changes
onInputValueChange(inputValue: string) => voidCalled as the text in the input changes — the filter query, not the value
allowCustombooleantrueWhether 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) => ReactNodeAdd “…”What that row says
clearablebooleanfalseShows a × that empties the field. A field that can be cleared in one click can be emptied by accident
emptyMessageReactNode'No matches'Shown in the popup when nothing matches and no value may be added
limitnumber-1The most rows the list will show at once. -1 is all of them
placeholderstringShown in the input while nothing is typed
labelReactNodeThe label, wired to the control by Base UI's Field
descriptionReactNodeHelper text
errorReactNodeError message. Its presence turns the control invalid and re-points the colour family at danger
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
startIconReactNodeContent before the input
fullWidthbooleanfalseStretches to the width of the container
removeLabel(label: string) => stringRemove …Accessible name of a chip's remove button. Receives the chip's label
clearLabelstring'Clear'Accessible name of the clear button
namestringIdentifies the field when a form is submitted
readOnlybooleanfalseShown but not changeable. Keeps its colour and edge, drains the saturation
disabledbooleanfalseUnavailable. Drops the colour family for neutral grey

Options are data

The same shape Select takes, with one difference:

ts
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/listbox wiring, arrow-key navigation across both the list and the chips, and the hidden input that makes the field submit with a form.
  • label becomes 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.

Released under the MIT License