Combobox
Reach for Combobox when the value still comes from a fixed set of options but the set is long enough that scanning it is work: a country, a teammate, a label. It renders a text input that filters a listbox as you type, and it only ever produces values from the list — for free-form text with suggestions, that is an autocomplete, which this is not. For a short list with no input, use Select.
Pass items to <Combobox.Root> — filtering, the function-child form of
<Combobox.List> and <Combobox.Empty> all depend on it. Items can be plain
strings, or objects: a { value, label } shape displays its label in the
input automatically, any other shape needs itemToStringLabel.
Import
import { Combobox } from "@forte-ui/react";Examples
Sizes
size on <Combobox.InputGroup> sets the field's height floor, inline
padding and font size, on the same --forte-control-* scale as Input and
Select.Trigger — the three line up on one row. The popup is unaffected.
Variants
variant changes only how much weight the field carries: outline reads as a
form control, soft as a filled field, ghost as an inline affordance.
Multiple selection
multiple on <Combobox.Root> turns the value into an array and keeps the
popup open across picks — including picks made after typing a filter, so
"search once, tick three results" works and the query survives to let you.
Render the selection as chips: <Combobox.Chips>
wraps a <Combobox.Value> function child that maps the values to
<Combobox.Chip>s, with the input rendered last so it shares a line with the
final chip. Backspace in the empty input removes the last chip; ArrowLeft
walks into the chips, where Delete removes the highlighted one.
Input inside the popup
Render <Combobox.Input> inside <Combobox.Popup> and the combobox becomes a
searchable select: a <Combobox.Trigger> holding <Combobox.Value> and
<Combobox.Icon> anchors the popup, and the input waits at the top of it. The
input restyles itself as a search row in this position — same part, no extra
props. This is the pattern <Combobox.Label> exists for: the trigger is the
form control here, and the label focuses it on click.
Grouped options
Pass grouped items ({ value, items } objects) to items on
<Combobox.Root>, give each <Combobox.Group> its items, and render them
through <Combobox.Collection> — that is what lets a query narrow each group
while the headings stay put, and empty groups drop out whole.
Async search
When the options live on a server, do the filtering there: pass the results as
items, set filter={null} so the combobox does not filter a second time
client-side, and drive the request from onInputValueChange.
<Combobox.Status> is the part that narrates the wait — it is a polite live
region, so "Searching…", errors and hints reach screen readers as they change.
Combobox.useFilter provides the Intl.Collator-backed matcher the fake
server here uses.
The same shape works with multiple — the selected people ride along in
items so their chips survive searches that no longer return them:
Creatable
A combobox only produces values from its list, so "create a new one" is a row
in the list, not free typing: while the query matches nothing exactly, a
synthetic Create "…" item rides along, and choosing it (or pressing Enter
with nothing highlighted) opens a dialog instead of selecting. The real item
is added and selected when the dialog's form submits.
Virtualized
Past a few thousand items the mount cost of opening dominates, and windowing
the list is what keeps it fast. Set virtualized on <Combobox.Root> so
highlight and ARIA bookkeeping run on indexes rather than rendered elements,
read the currently filtered items with Combobox.useFilteredItems, and hand
the list element to the virtualizer — the list is already the component's
scroll container, so no inner scroller is needed. Up to roughly a thousand
items, wrapping each item in React.memo is the simpler fix.
Accessibility
| Key | Behaviour |
|---|---|
| Arrow Down | Opens the popup from the input; moves the highlight down one item once open. The input stays focused — the highlight is virtual. |
| Arrow Up | Opens the popup from the input; moves the highlight up one item once open. |
| Enter | Selects the highlighted item. In a form, a combobox with a closed popup lets Enter submit as usual. |
| Esc | Closes the popup without changing the value. |
| A–Z | Types in the input and filters the list as you type. There is no typeahead — the input is the typeahead. |
| Backspace | Edits the query; in an empty multiple-selection input it removes the last chip. |
| Arrow Left / Right | Moves the caret; from the edge of an empty multiple-selection input, moves the highlight into the chips, where Delete removes one. |
The highlight loops by default: Arrow Down from the last item returns to the
input, and another Arrow Down starts the list over (loopFocus on
<Combobox.Root> turns this off). data-highlighted marks the highlighted
item while real DOM focus stays on the input; pointer hover sets the same
attribute (highlightItemOnHover, on by default), so style rows off
[data-highlighted], not :hover, or keyboard users get no highlight.
Theming
Every --forte-combobox-* property below is declared on the surface that owns
it, and an element's own declaration beats an inherited value — so setting one
on :root, on a theme scope or on any ancestor has no effect. The popup is
portalled to <body> as well, so an ancestor of the field is not even in
its tree. Set each property on the part that declares it, through that part's
className (an unlayered rule beats the library's @layer forte.components, whatever its specificity) or an inline style:
Combobox.Labeldeclares the fourlabel-*properties.Combobox.InputGroupdeclares everyfield-*property plus--forte-combobox-input-min-widthand--forte-combobox-button-size— the input and the icon buttons read those but never redeclare them, so the group is the place to set them.Combobox.Triggerdeclares thetrigger-*properties; they style its standalone select-like shape only.Combobox.Chipdeclares thechip-*properties.Combobox.Popupdeclares thepopup-*and motion properties plus--forte-combobox-item-px,--forte-combobox-item-gapand--forte-combobox-item-indicator-size— the layout knobs the list, the items, the group labels and the status rows read but never redeclare.positionerClassNamereaches--forte-combobox-z-index, the only property declared on the positioner.Combobox.Listdeclares--forte-combobox-list-padding-y;Combobox.Itemdeclares theitem-*colour and font properties.
The global tokens these resolve to — --forte-color-*, --forte-control-*,
--forte-radius-*, --forte-space-* — are the exception: the component only
reads those and never redeclares them, so re-pointing them on :root or on a
theme scope does re-skin every combobox.
| Property | Controls | Default |
|---|---|---|
--forte-combobox-label-fg | Label text colour | var(--forte-color-foreground) |
--forte-combobox-label-font-size | Label font size | var(--forte-font-size-2) |
--forte-combobox-label-font-weight | Label font weight | var(--forte-font-weight-medium) |
--forte-combobox-label-gap | Space between label and control | var(--forte-space-1) |
--forte-combobox-field-height | Field height floor (reset per size); the field GROWS past it when chips wrap, which is why it is a min height and not a height | var(--forte-control-h-md) |
--forte-combobox-field-px | Field inline padding (reset per size) | var(--forte-control-px-md) |
--forte-combobox-field-font-size | Field font size (reset per size) | var(--forte-font-size-2) |
--forte-combobox-field-gap | Gap between the input, the chips and the buttons | var(--forte-space-1) |
--forte-combobox-field-radius | Field corner radius | var(--forte-radius-control) |
--forte-combobox-field-border-width | Field border width | 1px |
--forte-combobox-field-width | Default field width | 16rem |
--forte-combobox-input-min-width | Smallest width the inline input keeps before wrapping under the chips | var(--forte-space-8) |
--forte-combobox-button-size | Size of the icon buttons (Trigger, Clear) inside the field | calc( var(--forte-combobox-field-height) - var(--forte-space-1) * 2 - var(--forte-combobox-field-border-width) * 2 ) |
--forte-combobox-button-radius | Corner radius of the icon buttons (Trigger, Clear) inside the field | max( 0px, calc(var(--forte-combobox-field-radius-painted) - var(--forte-space-1)) ) |
--forte-combobox-chip-inset | Inline space a chip holds before its own label. Set it HERE, on the group — a chip's own copy cannot reach the padding that pairs with it | var(--forte-space-2) |
--forte-combobox-field-bg | Field background (reset per variant) | var(--forte-color-background) |
--forte-combobox-field-bg-hover | Field background on hover | var(--forte-color-panel-hover) |
--forte-combobox-field-fg | Field text colour | var(--forte-color-foreground) |
--forte-combobox-field-border-color | Field border colour (reset per variant, and by data-invalid) | var(--forte-color-border) |
--forte-combobox-field-border-color-hover | Field border colour on hover | var(--forte-color-border-strong) |
--forte-combobox-trigger-height | Trigger height (reset per size; standalone shape only) | var(--forte-control-h-md) |
--forte-combobox-trigger-padding-x | Trigger inline padding (reset per size; standalone shape only) | var(--forte-control-px-md) |
--forte-combobox-trigger-font-size | Trigger font size (reset per size; standalone shape only) | var(--forte-font-size-2) |
--forte-combobox-trigger-gap | Gap between value and icon (standalone shape only) | var(--forte-control-gap) |
--forte-combobox-trigger-radius | Trigger corner radius (standalone shape only) | var(--forte-radius-control) |
--forte-combobox-trigger-border-width | Trigger border width (standalone shape only) | 1px |
--forte-combobox-trigger-bg | Trigger background (reset per variant; standalone shape only) | var(--forte-color-background) |
--forte-combobox-trigger-bg-hover | Trigger background on hover | var(--forte-color-panel-hover) |
--forte-combobox-trigger-bg-active | Trigger background while pressed or open | var(--forte-color-panel-active) |
--forte-combobox-trigger-fg | Trigger text colour | var(--forte-color-foreground) |
--forte-combobox-trigger-border-color | Trigger border colour (reset per variant; standalone shape only) | var(--forte-color-border) |
--forte-combobox-chip-bg | Chip background | var(--forte-color-panel) |
--forte-combobox-chip-fg | Chip text colour | var(--forte-color-foreground) |
--forte-combobox-chip-bg-highlighted | Background of the chip keyboard navigation has reached | var(--forte-color-primary-soft) |
--forte-combobox-chip-fg-highlighted | Text colour of that chip | var(--forte-color-primary-text) |
--forte-combobox-chip-radius | Chip corner radius | max( 0px, calc(var(--forte-combobox-field-radius-painted) - var(--forte-space-1)) ) |
--forte-combobox-chip-font-size | Chip font size | var(--forte-font-size-1) |
--forte-combobox-backdrop-z-index | Stacking of the optional backdrop layer | 49 |
--forte-combobox-z-index | Stacking of the positioner | 50 |
--forte-combobox-popup-bg | Popup surface colour | var(--forte-color-overlay) |
--forte-combobox-popup-fg | Popup text colour | var(--forte-color-foreground) |
--forte-combobox-popup-radius | Popup corner radius | var(--forte-radius-surface) |
--forte-combobox-popup-shadow | Popup shadow | var(--forte-shadow-4) |
--forte-combobox-popup-border-width | Width of the hairline that separates the popup from the page | 1px |
--forte-combobox-popup-border-color | Colour of that hairline | var(--forte-color-border) |
--forte-combobox-travel | How far the popup slides in from, per side | var(--forte-travel-sm) |
--forte-combobox-enter-scale | Scale the popup grows from and shrinks back to | var(--forte-scale-enter) |
--forte-combobox-enter-duration | Length of the opening gesture | var(--forte-duration-normal) |
--forte-combobox-enter-ease | Curve of the opening gesture | var(--forte-ease-spring-snappy) |
--forte-combobox-exit-duration | Length of the closing gesture | var(--forte-duration-fast) |
--forte-combobox-exit-ease | Curve of the closing gesture | var(--forte-ease-exit) |
--forte-combobox-item-px | Inline padding shared by items, group labels and status rows | var(--forte-space-3) |
--forte-combobox-item-gap | Gap between the indicator column and the item text | var(--forte-control-gap) |
--forte-combobox-item-indicator-size | Width of the check-mark column | var(--forte-space-4) |
--forte-combobox-list-padding-y | Block padding inside the scrolling list | var(--forte-space-1) |
--forte-combobox-item-fg | Item text colour | var(--forte-color-foreground) |
--forte-combobox-item-bg-highlighted | Background of the highlighted item | var(--forte-color-primary-soft) |
--forte-combobox-item-fg-highlighted | Text colour of the highlighted item | var(--forte-color-primary-text) |
--forte-combobox-item-fg-selected | Text colour of the selected item | var(--forte-color-primary-text) |
--forte-combobox-item-font-size | Item font size | var(--forte-font-size-2) |
--forte-combobox-group-label-fg | Group label text colour | var(--forte-color-foreground-muted) |
--forte-combobox-group-label-font-size | Group label text size | var(--forte-font-size-1) |
--forte-combobox-group-label-tracking | Group label letter-spacing | 0.06em |
--forte-combobox-group-label-mt | Space above a group label, dropped on the first group in the list | var(--forte-space-2) |
API reference
Combobox.Root
Groups every part and owns the value, the input value and the open state. It
renders no DOM element, so it takes neither className nor ref, and it
forwards Base UI's own Combobox.Root props unchanged — including value,
defaultValue, onValueChange, inputValue, onInputValueChange, items,
multiple, filter, filteredItems, itemToStringLabel,
isItemEqualToValue, autoHighlight, openOnInputClick, limit, grid,
inline, virtualized, name, disabled, readOnly, required and
modal. Both generics are preserved, so <Combobox.Root<Country>> types
onValueChange, items and defaultValue for you.
Combobox.Label
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.InputGroup
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
fullWidth | boolean | false | Stretch the field to fill the width of its container. |
size | ComboboxSize | md | Size of the field. Height, inline padding and font size move together, and the actual numbers follow the ambient `data-forte-density` setting. Matches `Input` and `Select.Trigger`, so the three line up on one row. |
variant | ComboboxVariant | outline | How much visual weight the field carries. `outline` reads as a form control, `soft` as a filled field, `ghost` as an inline affordance. |
Combobox.Input
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | Contents of the trigger. Defaults to a caret pair, the right children for the icon-button form inside an `<Combobox.InputGroup>` — an icon-only trigger still needs an `aria-label`. In the input-inside-popup pattern pass `<Combobox.Value>` and `<Combobox.Icon>` instead, and name the control with a `<Combobox.Label>`. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
fullWidth | boolean | false | Stretch the standalone trigger to fill the width of its container. |
size | ComboboxSize | md | Size of the standalone trigger. Ignored inside an `<Combobox.InputGroup>`, where the group's `size` decides. |
variant | ComboboxVariant | outline | How much visual weight the trigger carries. Only in play when the trigger stands alone (the input-inside-popup pattern); inside an `<Combobox.InputGroup>` the group owns the chrome and the trigger is a plain icon button. |
Combobox.Value
Renders no DOM element. Takes placeholder, or a function as children to
format the current value — the multiple-selection demos above use exactly
that to render chips.
Combobox.Icon
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | <CaretUpDownIcon /> | Icon to render. Defaults to a caret pair whose halves spread apart while the popup is open. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Clear
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | <XIcon /> | Contents of the button. Defaults to an X glyph; the button is icon-only either way, so it still needs an `aria-label`. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Chips
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Chip
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.ChipRemove
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | <XIcon /> | Contents of the button. Defaults to an X glyph; give the button an `aria-label` naming the value it removes. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Popup
Renders the floating half of the anatomy — portal, optional backdrop,
positioner and the popup surface — and hands you the inside: unlike
Select.Popup it does not wrap its children in a list, because what sits
next to the list is the point of a combobox.
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | start | How the popup aligns along the chosen side. |
alignOffset | number | OffsetFunction | 0 | Extra offset in pixels along the alignment axis. |
backdrop | boolean | false | Render a dimming layer behind the popup. Off by default — a combobox popup is a listbox, not a dialog. |
children | ReactNode | The popup's contents — typically `<Combobox.Empty>` followed by `<Combobox.List>`, plus `<Combobox.Status>` for async lists or an `<Combobox.Input>` first for the input-inside-popup pattern. | |
className | string | Additional class name(s) for the popup surface. Applied after the internal styles so consumer utilities win without needing `!important`. | |
collisionPadding | Padding | 5 | Space to keep between the popup and the edge of its collision boundary. |
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | Render the popup into a different container instead of `<body>`. | |
positionerClassName | string | Additional class name(s) for the positioner — the absolutely positioned wrapper around the popup. Use this to reach `--forte-combobox-z-index`, the only knob declared on the positioner. | |
side | Side | bottom | Which side of the anchor the popup opens on. |
sideOffset | number | OffsetFunction | 8 | Gap in pixels between the anchor and the popup. The default clears the focus ring rather than sitting flush against it: unlike `Select`, focus stays on the field while the popup is open, so the group's two-tone ring (`--forte-focus-ring-offset` + `--forte-focus-ring-width`, 4px together) is painted in exactly the gap this measures. |
Combobox.List
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((item: any, index: number) => ReactNode) | The items, groups and separators — or a function `(item, index) => ReactNode` that renders each item Base UI's filtering lets through. The function form is what keeps the list in sync with the query; it requires `items` on `<Combobox.Root>`. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Empty
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Status
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Item
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The item's label. Rendered next to the indicator column; give the item a plain-text `aria-label` if this is not text. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Whether the item ignores user interaction. Disabled items stay in the list and stay announced. |
indicator | ReactNode | <CheckIcon /> | What marks the item as selected. Rendered inside `<Combobox.ItemIndicator>`, which only mounts while the item is selected. |
value | unknown | null | The value this item selects. Objects are matched by `Object.is` unless `<Combobox.Root>` is given `isItemEqualToValue`; a `{ value, label }` shape displays its `label` in the input automatically. |
Combobox.Group
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.GroupLabel
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Separator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
orientation | Orientation | horizontal | Orientation of the rule. A combobox list is vertical, so the separator between two groups runs horizontally. |
Combobox.Row
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Combobox.Collection
Renders no DOM element. Takes a function child (item, index) => ReactNode
and renders the filtered items of the nearest <Combobox.Group> — a flat
list passes the function straight to <Combobox.List> instead. A direct
re-export of Base UI's part.
Combobox.useFilter
Returns Intl.Collator-backed matchers — contains, startsWith,
endsWith — for filtering externally. Pass the result to filter on
<Combobox.Root>, or use a matcher inside your own search, as the async
demos above do. Also exported standalone as useComboboxFilter.
Combobox.useFilteredItems
Returns the items the combobox is currently showing after its internal
filtering — what a virtualizer should measure. Must be called from a
component rendered inside <Combobox.Root>. Also exported standalone as
useComboboxFilteredItems.