DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CheckboxRadioSwitchTextareaNumberFieldOTP InputSliderCircularSliderRatingSelectPickerComboboxSearchSearchSuggestTokenizerSegmentedControl
AI agentsContributing
  1. Components
  2. Combobox

Combobox

Filterable select bound to an option list — single or multi-select with chips, groups, async loading, creatable rows, and the full field anatomy.

$ pnpm add @tessinaui/ui

When to use

  • A long list people would search rather than scan: countries, time zones, people, tags. The filter input is the core of the control.
  • A name the user already knows. Typing beats scrolling when people can recall the option and only need to find it.
  • Remote or changing data. Filter on the server and show loading and error states in the popup (Async loading).
  • Several values from the same list with multiple: chosen values become removable chips, and every chip comes from the option list.
  • Letting people add a value that is not listed. A "Create" row promotes the typed text to a real, selected option (Creatable).

When not to use

  • For a short, known list. Use Select. It is also the right choice when the trigger should show the chosen value as a button rather than an editable input; its multiple already summarises the count.
  • For two to four choices that should stay visible. Use Radio or SegmentedControl.
  • For commands and navigation. A centred overlay of actions with shortcut hints is Command. Rows that act (delete, invite, navigate) belong in DropdownMenu.
  • For a search box. A page-level search with history, scopes and a submit action is Search; the same field with a suggestions popup, where nothing is committed, is SearchSuggest. The combobox-recents example shows where the boundary runs.
  • For free-form tokens. When arbitrary text becomes chips (emails, keywords) with paste and comma splitting, use Tokenizer.
  • For a compact unit or currency selector attached to an input. Use FieldDropdown.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  ComboboxRoot,
  ComboboxInputGroup,
  ComboboxInput,
  ComboboxTrigger,
  ComboboxClear,
  ComboboxPopup,
  ComboboxList,
  ComboboxItem,
  ComboboxEmpty,
} from "@tessinaui/ui";
example.tsx
<ComboboxRoot items={fruits} label="Fruit" value={value} onValueChange={setValue}>
  <ComboboxInputGroup>
    <ComboboxInput placeholder="Search…" />
    <ComboboxClear />
    <ComboboxTrigger />
  </ComboboxInputGroup>
  <ComboboxPopup>
    <ComboboxList>
      {fruits.map((f) => (
        <ComboboxItem key={f} value={f}>{f}</ComboboxItem>
      ))}
    </ComboboxList>
    <ComboboxEmpty />
  </ComboboxPopup>
</ComboboxRoot>

Pass the option collection through items — that is what the built-in filter narrows as the user types. Static children without items render fine but do not filter themselves.

Examples

Default

A single-select combobox with a label, clear button, and filterable list.

Multiple

Multi-select mode renders chosen values as removable chips inside the input, and option rows switch to checkbox visuals. Each chip's remove button is a square of the chip's height, so it is at least 24px under a mouse; on touch its target is 44px, and wrapped chip rows part far enough that no row covers the one above it. A chip never grows past its row: in a narrow field a value wider than the row ellipsizes, and the full value stays the chip's accessible name and shows in the open list.

Grouped

Options organized under group labels, with separators between sections.

Sizes

Five height scales — xs to xl — on the field family's shared type ladder (12/14/14/16/16, 16px floor on phones).

Validation

errorMessage replaces the supporting text, implies the error intent, sets aria-invalid, and is announced politely. required stars the label; optional suffixes it.

Ghost variant

Borderless fields for toolbars and property rows — with visuallyHiddenLabel keeping the accessible name.

Underline variant

variant="underline" draws the Field family's bottom-rule shell: square whatever rounded says, no fill in any state, and focus or an open popup shown as an inset 2px bottom band in the intent colour instead of the ring.

On-color

Inside a colored Surface the tone resolves automatically: well, chips, rings and meta text all read the surface contract.

States

Disabled, read-only (focusable, value locked), and disabled options inside an enabled list.

Trailing meta

trailingText puts muted meta on the row end — dial codes, counts, prices — outside the accessible name. Leading flags via leadingIcon.

Match highlight

ComboboxHighlight bolds the matched substring while the accessible name stays whole.

Checkbox items

Multi-select filter panels: select-all / clear-all in the pinned footer with a live "n/N" count.

Max selection

Cap the selection in onValueChange — removals always pass, the helper counts what's left, and full lists disable the rest.

Creatable

Unknown input gets a "Create ‹query›" row that promotes the text to a real, selected option.

Async loading

Debounced remote search: min-characters hint, ComboboxStatus while loading, an error state with retry, and a result count in the footer.

Recents

openOnInputClick surfaces recent and trending groups before the user types.

Empty state with action

ComboboxEmpty accepts arbitrary children — pair the "no results" message with a way forward.

Footer action

A pinned row under the list for manage/config links — it never scrolls away.

Panel mode (search inside the popup)

A button-shaped trigger with the search input inside the popup — the branch-picker pattern.

Scrollable

Long lists cap their height and scroll inside the popup.

Loading

Skeletons that mirror the real anatomy — including the chip row in multiple mode.

RTL

dir="rtl" mirrors the field: text starts on the right, and the clear button and chevron move to the left end. The positioner receives the same dir, so the popup lines up with the field's right edge. Leave dir unset and the combobox inherits the page's direction.

API Reference

ComboboxRoot props

All Base UI Combobox.Root props are forwarded (items, value, onValueChange, inputValue, onInputValueChange, filter, filteredItems, multiple, limit, locale, autoHighlight, openOnInputClick, virtualized, grid, inline, actionsRef, name, form, …). form reaches the hidden input, so a combobox rendered outside its <form> element still submits with it. House props:

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Field height; text reads the family ladder
intent"none" | "error" | "warning" | "success" | "info""none"Border, ring and supporting color
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Radius cascade — popup and items follow
variant"outline" | "filled" | "ghost" | "underline""outline"Bordered well, a tinted well with no outline until focus or an intent, borderless toolbar field, or the square bottom rule that focuses and opens with an inset band (ignores rounded)
tone"default" | "on-color"from SurfaceOn colored plates; resolves from Surface context
labelPlacement"top" | "inside" | "start""top"Logical label layout
labelWidth"sm" | "md" | "lg" | "xl" | "auto" | CSS length"md"Width of the start-label column, so sibling fields in a form share one control column — sm 6rem · md 8rem · lg 11rem · xl 14rem. Labels wrap, never truncate. Only applies with labelPlacement="start", from sm up
labelstring—Label text, associated via htmlFor
requiredboolean—Star on the label + required on the input
optionalboolean—"(optional)" label suffix; ignored with required
infoTextstring—Info hint at the label-row end, in the describedby chain
supportingTextstring—Helper below the field (polite region)
errorMessagestring—Replaces the helper, implies error intent, sets aria-invalid
visuallyHiddenLabelboolean—Keep the name for AT, hide the label visually
popupPadding"narrow" | "default" | "wide""default"Popup inner padding
dir"ltr" | "rtl"inheritedLogical direction. Stamped only when set; unset, read off the field and fed to Base UI and the popup
stringsPartial<ComboboxStrings>English, Arabic under dir="rtl"Override the trigger button's name — showSuggestions, or showSuggestionsFor ("Show {label} suggestions") when the combobox has a label — the clear button's (clear, clearField), each chip's remove button (remove) and the optional marker. The language comes from dir alone: leave it unset and the combobox inherits the page's direction but keeps English
labelPosition——Deprecated — use labelPlacement
width——Deprecated — use popupPadding
wrapperClassName——Deprecated — use className

Without a label, give the input its own aria-label — the input must have a name.

ComboboxItem props

PropTypeDescription
valueanyThe value this item represents
disabledbooleanPrevents selection
leadingIconReactNodeIcon before the label
leadingAvatarReactNodeAvatar before the label
descriptionstringSecond line — outside the name, wired as the description
trailingTextReactNodeMuted end-of-row meta, outside the name

In multiple mode every item renders a checkbox visual; single-select items render the trailing checkmark.

ComboboxHighlight props

PropTypeDescription
textstringThe full option text
querystringSubstring to bold (case-insensitive, every occurrence)

ComboboxLeadingIcon props

PropTypeDescription
onClick() => voidRenders a real focusable <button> — name it with aria-label. Omit for a decorative slot

ComboboxPopup props

PropTypeDescription
positionerPropsPositioner propsSide, align, offsets, and style — the popup defaults to the anchor's width via --anchor-width; override it here

ComboboxList

The listbox and the scrollport are the same element — Base UI puts role="listbox" and the id aria-controls resolves against on this node — so the list keeps a native scrollport wearing the house scrollbarSkin rather than composing the ScrollArea component. ScrollArea cannot re-measure a list whose content height changes from filtering; see When you can't use it: scrollbarSkin. It caps at min(18rem, 100dvh - 6rem) and scrolls.

ComboboxFooter

A pinned row under ComboboxList — manage links, bulk actions, result counts. It bleeds to the popup edge and never scrolls with the list.

ComboboxFilterTabs / ComboboxFilterTab

PropTypeDescription
value / onValueChangestring / (v) => voidControlled active filter
countnumberCount badge on a tab

Plain aria-pressed toggles in a labelled group — see the accessibility note.

ComboboxSkeleton props

PropTypeDefaultDescription
size / roundedas Rootmd / fullGeometry from the same maps
showLabelbooleanfalseLabel row
labelPlacement"top" | "inside" | "start""top"Mirrors the real layout
labelWidth"sm" | "md" | "lg" | "xl" | "auto" | CSS length"md"Width of the start-label column, so sibling fields in a form share one control column — sm 6rem · md 8rem · lg 11rem · xl 14rem. Labels wrap, never truncate. Only applies with labelPlacement="start", from sm up
showSupportingTextbooleanfalseHelper row
showLeadingIconbooleanfalseLeading icon circle
showChipsbooleanfalseChip pills for multiple mode

Accessibility

The input itself carries role="combobox" with aria-expanded, aria-controls and aria-activedescendant; DOM focus never leaves the input while arrowing through options. The visible (or sr-only) label names the input via htmlFor; supportingText / errorMessage / infoText join aria-describedby, the meta row is a polite live region, and errorMessage sets aria-invalid. Option rows announce their label alone — description becomes the option's description, trailingText stays out of both.

Keyboard

KeyAction
Tab / Shift+TabInto / out of the input (the chevron button is not a tab stop)
↓ / ↑Open the popup; move the highlight (wraps)
EnterAccept the highlighted option and close
EscClose the popup
Home / EndMove the text caret, not the list highlight
Printable keysType and filter — Space types a space, it never selects
← / → on an empty multi inputWalk the chips
Backspace on an empty multi inputRemove the last chip

AT matrix

ATStatus
VoiceOver (macOS, Safari)Failed 2026-09-14 on one path. Typing to filter, or opening with the trigger, reads each option ("Apple (1 of 10)", "Banana (2 of 10)") and the committed value. Opening the empty input with ArrowDown reads "expanded list 10 items" and then no option at all: the list opens and highlights in the same moment, and VoiceOver does not follow the highlight into a popup that has just appeared. Reported upstream as mui/base-ui#5721; until it is fixed, type a character or use the trigger. Transcript: tests/qa/vo-transcripts/combobox.md
NVDAUntested — cannot run on this hardware
JAWSUntested — cannot run on this hardware

Known limitations

  • VoiceOver + Safari: ArrowDown on the empty input reads no options. Base UI opens the list and highlights an option in the same moment, and VoiceOver does not follow that highlight into a popup that has just appeared. Typing a character or using the trigger reads every option. Tracked upstream in mui/base-ui#5721.
  • Filter tabs are a pointer/AT refinement. The popup keeps DOM focus on the input, so the tab buttons are not keyboard-reachable while the popup is open. They are deliberately aria-pressed toggles, not a tablist. Keep a keyboard route to the same filtering — typing, or controls outside the popup.
  • Interactive rows inside the footer (ComboboxFooter) are reachable by pointer and AT cursor, not by Tab while the popup is open.

Migrating

BeforeAfter
labelPosition="outside-top"labelPlacement="top"
labelPosition="inside"labelPlacement="inside" — now renders a real floating label
labelPosition="outside-left"labelPlacement="start"
width="narrow | wide"popupPadding="narrow | wide"
wrapperClassNameclassName

Deprecated props keep working for one minor. Visual changes in this release: the focus ring keeps the border underneath (field model), input/item text adopts the shared field ladder (sm 12→14, lg 14→16 on desktop), rounded gains xl, and the pill popup cap is now the panel radius.

PickerSearch

On this page

When to useWhen not to useInstallationUsageExamplesDefaultMultipleGroupedSizesValidationGhost variantUnderline variantOn-colorStatesTrailing metaMatch highlightCheckbox itemsMax selectionCreatableAsync loadingRecentsEmpty state with actionFooter actionPanel mode (search inside the popup)ScrollableLoadingRTLAPI ReferenceComboboxRoot propsComboboxItem propsComboboxHighlight propsComboboxLeadingIcon propsComboboxPopup propsComboboxListComboboxFooterComboboxFilterTabs / ComboboxFilterTabComboboxSkeleton propsAccessibilityKeyboardAT matrixKnown limitationsMigrating