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

Field
Type
Meaning
Notes
id
WidgetId
Stable identity for the editor, listbox, options, and anchor.
Required. Use a durable id for dynamic or reorderable fields.
value
String
Controlled editor text.
May be free text or the selected label, according to the product model.
placeholder
Option<TextContent>
Hint shown while value is empty.
Supports literal or localized text and is forwarded to the retained TextInput.
items
Vec<String>
Current suggestions in display order.
Supply the filtered list; an empty list keeps the popup unmounted.
is_open
bool
Requested controlled popup state.
The effective open state also requires at least one item.
width
Option<f32>
Logical width for the input and matching popup.
None leaves input sizing to layout and still asks the popup to match its anchor.
max_popup_height
Option<f32>
Height limit before option scrolling.
Defaults to 240.0.
on_input
Option<ActionEnvelope>
Action dispatched for text edits.
Read TextChange from the reducer input; the bound payload is preserved.
on_select
Option<Arc<dyn Fn(String) -> ActionEnvelope + Send + Sync>>
Builds an action for an activated option.
Receives the option label.
on_toggle
Option<ActionEnvelope>
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.
Previous
Checkbox