- Keys
ArrowDown / ArrowUp- When
- trigger, closed
- Effect
- Opens the list and highlights the first/last matching option.
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
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
valueand displaylabel. Filtering keeps the option order and active option in sync with the visible list. -
Selected value:
valueis controlled, whiledefaultValuestarts an uncontrolled selection. Selection requests report the proposed value and reason; a controlled owner adopts it on its next update. -
Open state:
openanddefaultOpenfollow the same controlled and uncontrolled shape. Escape and outside click close the non-modal list without changing the selected value. -
Filter: typing updates
inputValueand 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
ArrowDown / ArrowUpArrowDown / ArrowUpEnterHome / EndEscapePrintable charactersClick outside- 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
-
combobox.contract.ts, the shared browser contract, run against the vanilla mount and all five framework adapters, including accessibility scans. -
combobox.spec.md, the reviewable state, anatomy, ARIA, keyboard, forms, and SSR contract. - DropdownMenu: the non-modal overlay walkthrough. The closest sibling for layer-owned Escape and outside-click behaviour.
- Select: the form-selection walkthrough. The closest sibling for options, value changes, and hidden form inputs.