Components

Combobox

Headless editable combobox primitives for filtering suggestions and selecting a value from a listbox.

When to Use

Use Combobox when users should type to find an option, such as choosing a city from a long list. Use Select when users must choose from a fixed list without typing. Use Listbox when the choices should remain visible and no text input is needed. Enable freeSolo only when typed values outside the option list are valid.

Features

  • Controls selection, input text, and open state independently.
  • Filters, groups, highlights, and selects options.
  • Supports free-form values, loading, empty state, clearing, and disabled items.
  • Positions Content with Floating UI and portals on request.
  • Provides generated combobox/listbox ARIA relationships.
  • Submits the selected value through a hidden named input.
  • Exposes reusable option filtering, labeling, grouping, and navigation helpers.

Import

import { Combobox } from "@flowstack-ui/atom";

Anatomy

<Combobox.Root>
  <Combobox.Label />
  <Combobox.Input />
  <Combobox.Clear />
  <Combobox.Portal>
    <Combobox.Content>
      <Combobox.Listbox>
        <Combobox.Group>
          <Combobox.Label />
          <Combobox.Item />
        </Combobox.Group>
        <Combobox.Empty />
        <Combobox.Loading />
      </Combobox.Listbox>
    </Combobox.Content>
  </Combobox.Portal>
</Combobox.Root>

useComboboxContext()
filterComboboxOptions()
getComboboxOptionLabel()
groupComboboxOptions()
getNextComboboxValue()

API Reference

Root

Owns all combobox state and renders no visible wrapper. When name is set, it adds an accessibility-hidden input containing the selected value.

PropTypeDefault
optionsComboboxOption[]required
valuestring | null-
defaultValuestring | nullnull
onValueChange(value: string | null) => void-
inputValuestring-
defaultInputValuestring""
onInputValueChange(value: string) => void-
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-
filterOptionsComboboxFilterfilterComboboxOptions
groupBy(option) => string-
freeSolobooleanfalse
clearOnSelectbooleanfalse
openOnFocusbooleantrue
loadingbooleanfalse
noOptionsTextReactNode"No options"
loadingTextReactNode"Loading"
disabledbooleanfalse
readOnlybooleanfalse
requiredbooleanfalse
invalidbooleanfalse
namestring-
formstring-

The hidden input receives name, form, value, and disabled state. Root does not attach native required validation to that hidden input.

Label

Labels Input when used outside Group and labels the option group when nested inside Group. It renders label outside a group and div inside one.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"combobox-label"

Input

Renders the editable input, controls the filter text, opens Content, and owns all combobox keyboard interaction.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
autoCompletestring"off"
ARIA attributeValues
role"combobox"
aria-expandedCurrent open state
aria-haspopup"listbox"
aria-controlsGenerated Listbox ID
aria-activedescendantHighlighted Item ID while open
aria-autocomplete"list"
aria-required"true" when required
aria-invalid"true" when invalid
Data attributeValues
[data-slot]"combobox-input"
[data-state]"open" | "closed"
[data-disabled]Present when disabled
[data-readonly]Present when read only
[data-invalid]Present when invalid

Clear

Clears the selected value and input, then returns focus to Input. It renders a button and is hidden and disabled when no selected value exists or Root is disabled/read-only.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelConsumer value or "Clear selection"
aria-hidden"true" while hidden
Data attributeValues
[data-slot]"combobox-clear"
[data-hidden]Present while hidden

Portal

Moves its children to document.body by default without a wrapper.

PropTypeDefault
containerElement | DocumentFragment | nulldocument.body
disabledbooleanfalse

Content

Renders and positions the open popup below Input with flip and viewport-shift collision handling. It owns no listbox role; place Listbox inside it.

PropTypeDefault
sideOffsetnumber4
Data attributeValues
[data-slot]"combobox-content"
[data-state]"open"
[data-positioned]Present after positioning

Listbox

Contains the available options and receives the ID referenced by Input.

ARIA attributeValues
role"listbox"
Data attributeValues
[data-slot]"combobox-listbox"
[data-state]"open" | "closed"

Group

Groups related options. A nested Label supplies its generated accessible name; native aria-label or aria-labelledby overrides that relationship.

ARIA attributeValues
role"group"
aria-labelNative label when supplied
aria-labelledbyConsumer ID or nested Label ID
Data attributeValues
[data-slot]"combobox-group"

Item

Represents one selectable option and registers its value in list order.

PropTypeDefault
valuestringrequired
labelstringValue or matching option label
disabledbooleanfalse
ARIA attributeValues
role"option"
aria-selectedCurrent selected state
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"combobox-item"
[data-state]"checked" | "unchecked"
[data-highlighted]Present when highlighted
[data-value]Item value
[data-disabled]Present when disabled

Empty

Renders only when filtering produces no options and Root is not loading. Its children default to noOptionsText.

Data attributeValues
[data-slot]"combobox-empty"

Loading

Renders only while Root is loading. Its children default to loadingText.

Data attributeValues
[data-slot]"combobox-loading"

useComboboxContext

Returns the current state and actions for advanced custom parts, including filteredOptions and groupedOptions. It must be called below Root. Prefer the namespaced parts for standard input, listbox, and option behavior.

filterComboboxOptions

Returns options whose case-insensitive display label contains the input text. An empty input returns the original options.

getComboboxOptionLabel

Returns option.label when present, otherwise option.value.

groupComboboxOptions

Groups options in encounter order using groupBy. Without groupBy, it returns one group with a null label.

getNextComboboxValue

Returns the next or previous value with wrapping. It returns null for an empty array and begins at the first/last value when the current value is absent.

Examples

Choose a City

import { Combobox, useComboboxContext } from "@flowstack-ui/atom";

const cities = [
  { value: "boston", label: "Boston" },
  { value: "chicago", label: "Chicago" },
  { value: "seattle", label: "Seattle" },
];

function CityOptions() {
  const { filteredOptions } = useComboboxContext();

  return filteredOptions.map((city) => (
    <Combobox.Item key={city.value} value={city.value}>
      {city.label}
    </Combobox.Item>
  ));
}

export function CityCombobox() {
  return (
    <Combobox.Root options={cities} name="city">
      <Combobox.Label>City</Combobox.Label>
      <Combobox.Input />
      <Combobox.Clear />
      <Combobox.Portal>
        <Combobox.Content>
          <Combobox.Listbox>
            <CityOptions />
            <Combobox.Empty />
          </Combobox.Listbox>
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  );
}

Free-Form Controlled Value

import { useState } from "react";
import { Combobox, useComboboxContext } from "@flowstack-ui/atom";

const tags = [{ value: "react" }, { value: "typescript" }];

function TagOptions() {
  const { filteredOptions } = useComboboxContext();

  return filteredOptions.map((tag) => (
    <Combobox.Item key={tag.value} value={tag.value} />
  ));
}

export function TagCombobox() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Combobox.Root options={tags} value={value} onValueChange={setValue} freeSolo>
      <Combobox.Label>Tag</Combobox.Label>
      <Combobox.Input />
      <Combobox.Content>
        <Combobox.Listbox>
          <TagOptions />
          <Combobox.Empty>No matching tag. Press Enter to create it.</Combobox.Empty>
        </Combobox.Listbox>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Accessibility

Combobox follows the WAI-ARIA Editable Combobox pattern. Provide an accessible Label, render every available value as an Item, and keep disabled options marked with disabled.

KeyDescription
ArrowDownOpens and highlights the next enabled Item, wrapping at the end.
ArrowUpOpens and highlights the previous enabled Item, wrapping at the start.
HomeHighlights the first enabled Item.
EndHighlights the last enabled Item.
EnterSelects the highlighted Item, or commits free-form text when enabled.
EscapeCloses an open popup; when already closed, clears selection and input.
TabCloses the popup and continues normal focus movement.

Changelog

See CHANGELOG.md.