Combobox
Primitive

Combobox

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

Live behavior

Combobox in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Interact with the specimen and inspect the behavior Atom contributes.

waiting for input

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

The behavior Atom owns before your product adds appearance.

  • Controls selection, input text, and open state independently.
  • Filters, groups, highlights, and selects options.
  • Provides a native disclosure Trigger and sizes Content from the full Control.
  • Supports free-form values, loading, empty state, clearing, and disabled items.
  • Positions Content with Floating UI and portals on request.
  • Commits outside dismissal on a completed activation, rejects cancelled or dragged pointer sessions, and supports consumer cancellation.
  • 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

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

Anatomy

tsx
<Combobox.Root>
  <Combobox.Label />
  <Combobox.Control>
    <Combobox.Input />
    <Combobox.Clear />
    <Combobox.Trigger />
  </Combobox.Control>
  <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-
defaultInputValuestringSelected option label or ""
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
validationBehavior"inline" | "native"Field/Form value or "native"
namestring-
formstring-

The transparent native value proxy receives name, form, the committed value, disabled state, and required state. It aligns to the visible Input and redirects browser validation focus there, so typed display text does not satisfy required validity until a value is committed. The visible Input receives Field state, the generated control ID, Field label and description relationships, and external form association. Uncontrolled value, input text, and open state return to their defaults on native form reset.

When composed inside Field.Root, Combobox inherits disabled, read-only, required, invalid, validation behavior, label, description, and error relationships. It also works standalone through the equivalent Root props. Fieldset.Root and Form.Root aggregate native/inline validity through the shared Field and form validation contracts.

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

Control

Wraps Input, Clear, and Trigger as the complete visible control. Content uses this element as its positioning reference and minimum inline size.

Data attributeValues
[data-slot]"combobox-control"
[data-state]"open" | "closed"
[data-disabled]Present when disabled
[data-readonly]Present when read only
[data-required]Present when required
[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

Trigger

Renders the button that toggles Content and returns focus to Input. Native focus scrolling remains enabled so a mobile browser can reveal Input when opening its virtual keyboard. Trigger inherits disabled/read-only behavior from Root or Field.

ARIA attributeValues
aria-expandedCurrent open state
aria-haspopup"listbox"
aria-controlsGenerated Listbox ID
Data attributeValues
[data-slot]"combobox-trigger"
[data-state]"open" | "closed"
[data-invalid]Present when invalid

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 Control with flip and viewport-shift collision handling. It owns no listbox role; place Listbox inside it.

PropTypeDefault
sideOffsetnumber4
onInteractOutside(event: OutsideInteractionEvent) => void-

onInteractOutside runs before dismissal. Calling its preventDefault() method keeps Content open without cancelling the original destination click. The event reports pointerType as "mouse", "touch", "pen", or "virtual".

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, registers its value in list order, and hides itself when its matching Root option is excluded by the active filter.

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

tsx
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

tsx
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

The aligned committed-value input owns required validity. A validation attempt is mirrored to the visible Input and Field. Inline behavior suppresses the browser bubble; native behavior keeps it and redirects focus to Input.

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

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.20.2

  • Allowed native focus scrolling when Trigger returns focus to Input, so mobile browsers can reveal the control above the virtual keyboard.

0.20.0

  • Added preventable Content.onInteractOutside and moved outside dismissal to the shared layer-aware completed-activation contract.

0.18.2

  • Added Control and Trigger parts, anchored Content to the full Control, and exposed inherited form state on the Control styling surface.
  • Made authored Items filter themselves from Root options and initialized display text from defaultValue when defaultInputValue is omitted.

0.18.1

  • Made touch and pen outside dismissal wait for a completed tap and cancel on movement, scrolling, or pointer cancellation.

0.6.16

  • Explicitly scrolled inline validation-directed focus into view.

0.6.15

  • Exposed inline validation-directed focus through [data-focus-visible] until blur.

0.6.13

  • Mirrored committed-value proxy validity to the visible Combobox, Field, and Form under the shared inline/native validation contract.

0.6.12

  • Moved native required validity to an aligned proxy holding the committed logical value, so display text alone no longer satisfies selection validity.

0.5.0

  • Added Field state, generated input ID, label, and description integration plus external-form and uncontrolled reset behavior.

0.2.0

  • Fixed option selection so pointer clicks, including already-selected options, close the listbox consistently, and clearOnSelect also applies to free-solo Enter commits.
  • Fixed openOnFocus so empty states can open on focus when Combobox.Empty is mounted.
  • Added shared dismissable layer Escape handling so Combobox closes before parent overlays when nested inside Dialog, Drawer, Modal, or Popover.
  • Fixed outside pointer dismissal so Combobox closes reliably when clicking outside the input or content during inspection-heavy renders.

0.1.0

  • Initial Atom release.