Docs Platform

Combobox

Combobox gives people a searchable choice from a list. The machine owns the selected value, filter text, open state, and active option. The headless layer adds accessible parts and form behaviour; styled components add Grassroot's visual recipe; vanilla mounts the same behaviour without a framework.

Explore the layers

Machine is pure state. Headless maps it to accessible DOM and ARIA. Styled is the ready-to-use framework component. Vanilla is the framework-free mount, for script islands and anywhere else with no framework to hand.

Install

npm i @grassroot/ui-machines

ts
import { createService } from "@grassroot/statechart"
import { comboboxMachine, type ComboboxCommand } from "@grassroot/ui-machines/combobox"
const options = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
]
const commands: ComboboxCommand[] = []
const service = createService(comboboxMachine, {
input: { options, defaultValue: ["react"], defaultOpen: false },
runCommand: (command) => { commands.push(command) },
})
service.start()
service.send({ type: "OPEN", reason: "trigger" })
service.send({ type: "SET_INPUT_VALUE", value: "vu" })
service.send({ type: "SELECT", value: "vue", reason: "keyboard" })
service.getSnapshot().context.value // ["vue"]
service.getSnapshot().context.inputValue // ""
commands.at(-1) // VALUE_CHANGE_REQUEST for "vue"
service.stop()

Behaviour contract

Every layer follows the same state and event rules.

  • Options: each option has a stable value and display label. Filtering keeps the option order and active option in sync with the visible list.
  • Selected value: value is controlled, while defaultValue starts an uncontrolled selection. Selection requests report the proposed value and reason; a controlled owner adopts it on its next update.
  • Open state: open and defaultOpen follow the same controlled and uncontrolled shape. Escape and outside click close the non-modal list without changing the selected value.
  • Filter: typing updates inputValue and recalculates the visible options. Clearing the filter restores the full option set.
  • Commands: value, open-state, and filter changes are reported to the connector. A real no-op emits no command.

Anatomy

The shared headless parts are:

Combobox.Label         data-part="label"         visible name for the field
Combobox.Control      data-part="control"      wrapper for input and trigger
Combobox.Input        data-part="input"        filter text input
Combobox.Trigger      data-part="trigger"      opens and closes the list
Combobox.Positioner   data-part="positioner"   overlay placement wrapper
Combobox.Content      data-part="content"      visible option collection
Combobox.Item         data-part="item"         one option row
Combobox.ItemText     data-part="item-text"     option label
Combobox.Empty        data-part="empty"        no-results message
Combobox.HiddenInput  data-part="hidden-input" form value

The trigger exposes the selected value and controls the list. The filter input owns the text cursor. The content uses an active descendant for keyboard navigation, and each item receives a stable id derived from its option value. Closed content is not rendered.

Keyboard

Keys
When
Effect
ArrowDown / ArrowUp
trigger, closed
Opens the list and highlights the first/last matching option.
ArrowDown / ArrowUp
open
Moves the active option and wraps at either end.
Enter
open with an active option
Selects the active option and closes the list.
Home / End
open
Moves to the first/last matching option.
Escape
open
Closes the list without changing the selected value; the non-modal layer owns Escape.
Printable characters
input focused
Updates the filter text and recalculates the visible options.
Click outside
open
Closes the list without changing the selected value.
Keys
ArrowDown / ArrowUp
When
trigger, closed
Effect
Opens the list and highlights the first/last matching option.
Keys
ArrowDown / ArrowUp
When
open
Effect
Moves the active option and wraps at either end.
Keys
Enter
When
open with an active option
Effect
Selects the active option and closes the list.
Keys
Home / End
When
open
Effect
Moves to the first/last matching option.
Keys
Escape
When
open
Effect
Closes the list without changing the selected value; the non-modal layer owns Escape.
Keys
Printable characters
When
input focused
Effect
Updates the filter text and recalculates the visible options.
Keys
Click outside
When
open
Effect
Closes the list without changing the selected value.

Forms

Add HiddenInput with a name to submit the selected value with a native form. The input is read-only because the Combobox state is the source of truth. A form reset returns the selection to its starting value and clears the filter.

Accessibility

Give the field a visible Label, or provide an accessible name with the trigger's aria-label or aria-labelledby. The trigger identifies the popup, the filter input is labelled for searching, and the content tracks the active option with aria-activedescendant. When no option matches, the Empty part gives the user a clear result instead of leaving an empty popup.

Evidence & further reading