Combobox
Combobox is the editable choice control for “type to narrow, then pick”
workflows. It combines a controlled TextInput with an anchored listbox while
keeping free-text edits distinct from confirmed selection.
Use Select when every valid value is already in a
short fixed list and typing arbitrary text should not be possible. Example
use fission::prelude::*;
use std::sync::Arc;
let assignee_field: Widget = FormControl {
id: Some(WidgetId::explicit("task.assignee-field")),
label: Some("Assignee".into()),
child: Combobox {
id: WidgetId::explicit("task.assignee"),
value: view.state().assignee_query.clone(),
placeholder: Some("Find a teammate".into()),
items: view.state().filtered_assignees.clone(),
is_open: view.state().assignee_open,
width: Some(280.0),
max_popup_height: Some(240.0),
on_input: Some(update_assignee_query_action),
on_select: Some(Arc::new(move |name| ActionEnvelope {
id: select_assignee_id,
payload: serde_json::to_vec(&SelectAssignee(name)).unwrap(),
})),
on_toggle: Some(toggle_assignee_popup_action),
}
.into(),
error: view.state().assignee_error.clone(),
helper: Some("Type a name to narrow the list.".into()),
required: true,
}
.into();
The application filters items from value. on_input reads the live edit
from ctx.input.text_change(), on_select receives the confirmed item label,
and on_toggle updates the controlled open state.
Anatomy and state ownership
The field is a single-line editable input with a trailing expand/collapse icon.
Its open portal contains a vertical, scrollable MenuContent::list_box surface;
each suggestion becomes a retained MenuActionItem::option. The popup requests
the anchor's width, uses Start alignment and a four-point gap, and is bounded to
max_popup_height.
The application owns all changing product state:
value is the text shown in the editor;
items is the already filtered list to present; and
is_open is the requested popup state.
Combobox does not fetch, rank, or filter choices internally. If items is
empty, it suppresses the popup even when is_open is true. A closed input with
an on_toggle action requests opening when it receives focus. While already
open, focus does not toggle it closed.
The input uses the complete active TextInput recipe. Option rows and their
default, hover, active, focus, and selected states use
theme.components.menu; that recipe also owns popup surface fill, border,
radius, spacing, and elevation. An exact first match between value and an item
label is marked selected and receives a trailing check.
Use the named anatomy when the editable field or options need more than the
compact string-list API:
use fission::prelude::*;
let current_assignee = view.state().assignee.as_deref();
let input = ComboboxInput::custom(TextInput {
value: view.state().assignee_query.clone(),
placeholder: Some("Find a teammate".into()),
on_input: Some(update_assignee_query_action),
semantics_identifier: Some("task.assignee.input".into()),
..Default::default()
});
let content = ComboboxContent::new(vec![
ComboboxGroup::new(vec![
ComboboxOption::option("Avery Stone", current_assignee == Some("Avery Stone"))
.id(WidgetId::explicit("task.assignee.avery"))
.description("Design")
.metadata("Available")
.on_select(select_avery_action)
.into(),
ComboboxOption::option("Morgan Lee", current_assignee == Some("Morgan Lee"))
.id(WidgetId::explicit("task.assignee.morgan"))
.description("Engineering")
.disabled(view.state().morgan_unavailable)
.on_select(select_morgan_action)
.into(),
])
.label("People")
.into(),
])
.max_height(240.0);
let assignee: Widget = ComboboxLayout::new(
WidgetId::explicit("task.assignee"),
input,
content,
)
.open(view.state().assignee_open)
.on_toggle(toggle_assignee_popup_action)
.width(280.0)
.into();
ComboboxLayout owns the controlled popup relationship and stable identity.
ComboboxInput retains a complete TextInput, including its edit action,
placeholder, validation, input policy, prefix, and suffix. ComboboxContent
owns the retained listbox entries and optional popup width and height bound.
Its ComboboxOption, ComboboxGroup, ComboboxLabel, and
ComboboxSeparator entries are the select/listbox anatomy under combobox names,
so descriptions, icons, metadata, groups, stable IDs, selected state, and
disabled state remain available without rebuilding a popup row.
The named API does not infer filtering or selection. Filter entries from the
controlled text, set each option's selected value from application state, and
close or update the field in its selection reducer. The popup opens only when
the input is enabled and the retained content contains at least one option.
Field reference
| | | |
|---|
| | Stable identity for the editor, listbox, options, and anchor. | Required. Use a durable id for dynamic or reorderable fields. |
| | | May be free text or the selected label, according to the product model. |
| | Hint shown while value is empty. | Supports literal or localized text and is forwarded to the retained TextInput. |
| | Current suggestions in display order. | Supply the filtered list; an empty list keeps the popup unmounted. |
| | Requested controlled popup state. | The effective open state also requires at least one item. |
| | Logical width for the input and matching popup. | None leaves input sizing to layout and still asks the popup to match its anchor. |
| | Height limit before option scrolling. | |
| | Action dispatched for text edits. | Read TextChange from the reducer input; the bound payload is preserved. |
| Option<Arc<dyn Fn(String) -> ActionEnvelope + Send + Sync>> | Builds an action for an activated option. | Receives the option label. |
| | Requests open or closed state. | Used by focus-to-open, outside click, Escape, and Tab dismissal. |
For ComboboxLayout, id, input, and content are required. is_open and
on_toggle remain controlled through .open(...) and .on_toggle(...);
.width(...) supplies a fallback width for both field and popup.
ComboboxContent::width(...) may set a popup-specific width, while
.max_height(...) bounds the scrolling listbox. ComboboxInput opens on focus
and supplies the standard trailing indicator when the underlying input has no
suffix by default; use .open_on_focus(false) or .show_indicator(false) to
opt out.
When open_on_focus is enabled and the wrapped input has no explicit
on_focus, the layout installs its toggle action to request opening. An
explicit TextInput::on_focus remains authoritative; when that field should
also open on focus, request the controlled open state from the same reducer.
Use .open_on_focus(false) when neither behavior is wanted.
Keyboard and focus behavior
Typing while the editor has focus follows normal TextInput editing and lets
the next conversion supply new suggestions. This is separate from option-list
navigation.
With an open listbox, the first ArrowDown makes the exact selected option
active when one exists, otherwise the first enabled option. ArrowUp likewise
prefers the selected option, then the last enabled option. Further vertical
arrow presses wrap through enabled options and skip disabled ones. Platform
focus remains on the editable field throughout; the active option is projected
through the combobox's active-descendant relationship and receives the option
focus treatment.
Typing after arrow navigation continues normal text editing and filtering; it
does not become listbox typeahead. Any edit clears the option active for the
previous query. Home, End, horizontal arrows, and Space retain their
normal text-editing meanings. Enter activates the current active option.
Escape dismisses the popup and clears that option while focus remains on the
editor. Tab or reverse Tab dismisses it and continues to the next eligible
page target. Options do not become extra page-level Tab stops. An outside
pointer press requests the same close action. A closed combobox installs no
input backdrop.
Semantics
The editor exposes an editable ComboBox role with its value, expanded state,
ListBox popup relationship, controlled listbox id, and current active
descendant. Before keyboard navigation, an exact single selection supplies the
active descendant. While navigating, the runtime-owned active option takes
precedence and is reconciled whenever options change; edits and popup closure
clear it. The compact API derives the initial selection from an exact
value/label match; the named API uses each option's explicit selected state.
The popup exposes a vertical ListBox; each selectable row is an Option with
explicit selected state. Groups and separators preserve their structural roles,
and disabled options expose disabled state without focus or activation.
Wrap the control in FormControl to attach a
visible label, helper or error description, required state, and invalid state to
the actual editable combobox node. The wrapper preserves the editor's value,
selection, relationships, and text actions. Responsive, target, and motion behavior
Choose width for the narrowest supported parent, and test long translated
suggestions. The bounded popup scrolls but does not virtualize a large data set;
filter or page very large sources before building items.
Automatic vertical placement prefers below the input, then moves above when the
lower placement would overflow and the upper placement fits. Horizontal
placement is clamped to the viewport. When exactly one option is selected, the
open flyout instead aligns that retained option's vertical center with the
input and clamps the complete surface within the viewport. The compact API
treats duplicate matching labels as ambiguous; the named API likewise requires
one explicit selected option before it chooses an alignment target.
Logical Start anchors to the input's left edge in left-to-right layout and its
right edge in right-to-left layout.
Layout direction controls the field anatomy, option rows, and flyout edge.
Paragraph bidi and shaping remain governed independently by the text system's
TextDirection; they are not inferred from layout direction.
The widget configures no popup animation. Any motion composed around the field
or its popup still follows the global Env::motion_preference, so the
combobox does not need its own preference branch. Its visual and semantic model
lowers through interactive targets. Static site and plain SSR output can emit
the closed field contract, but editing, popup state, filtering, and selection
require an interactive runtime or browser island.
Production checklist
Keep free-text edits and confirmed selection as separate reducer actions.
Derive items from authoritative application data and the controlled value.
Give the field a visible and semantic label with FormControl.
Verify virtual option arrow entry, disabled-option skipping, wrapping,
edit-after-navigation, Enter, Escape, both Tab directions, outside click,
and empty results.
Close the popup in the selection reducer when selection should complete the
interaction.