Skip to content

MultiSelect

MultiSelect is the ordinary multi-select list component. Like Select, it owns an internal ListState, but its interaction has two layers: Space toggles draft selection and triggers on_change; Enter submits the current selection and triggers on_select.

multi select demo

This recording is generated by docs/tapes/multi-select.tape. The example uses default_index to put the cursor on the first item, checks Format with Space, moves to Clippy, checks it, presses Enter to submit, and finally switches to the empty state.

cargo run --example multi_select

Press j/k to move, Space to toggle selection, Enter to submit, e to toggle the empty state, and q to quit.

MultiSelect<&'static str>(
    items: items,
    default_index: Some(0),
    highlight_symbol: "> ",
    empty_message: "No checks",
    on_change: move |items: Vec<&'static str>| {
        selected_count.set(items.len());
    },
    on_select: move |items: Vec<&'static str>| {
        submitted.set(format!("submitted {} checks", items.len()));
    },
)

default_index only controls the default cursor and does not automatically check an item. The selected set always comes from the user’s Space actions or from the external selected state you pass in.

Both on_change and on_select receive a Vec<T> already ordered by current item index:

  • on_change: triggered after every Space; useful for updating a preview panel, counter, or filter summary.
  • on_select: triggered after Enter; useful for confirming submit, closing a modal, or entering the next step.
on_change: move |items: Vec<&'static str>| {
    draft.set(selection_label(&items));
    submitted.set("draft changed".to_string());
},
on_select: move |items: Vec<&'static str>| {
    submitted.set(if items.is_empty() {
        "submitted none".to_string()
    } else {
        format!("submitted {} checks", items.len())
    });
},

This split fits search results, batch operations, settings panels, and multi-select confirmations in modals: users can try a draft selection, then submit with an explicit action.

By default, MultiSelect stores its own cursor and selected set. If business code needs to read or reset them across components, pass external state:

MultiSelect<&'static str>(
    state: list_state,
    selected: selected_indices,
    items: items,
    on_change: move |items| {
        summary.set(format!("{} selected", items.len()));
    },
)

selected is typed as State<HashSet<usize>>. When the list becomes shorter, the component automatically removes selected indices that are out of bounds; when the cursor is out of bounds, it clamps the cursor back to the last valid item. An empty list renders only empty_message and does not consume navigation or confirm keys, so the parent can still handle page shortcuts like e and q.

MultiSelect can receive width, height, margin, and offset, and forwards them to its internal Border:

MultiSelect<&'static str>(
    width: Constraint::Length(36),
    top_title: Line::from(" release checks ").centered(),
    bottom_title: Line::from(" draft selection ").centered(),
    border_style: Style::new().fg(Color::Cyan),
    highlight_style: Style::new().fg(Color::Black).bg(Color::Green),
    selected_item_style: Style::new().fg(Color::Yellow).bold(),
)

If a page has multiple selection controls, set active: false on the instance that should not currently respond to input. When placed inside a Modal subtree, the Current handler automatically belongs to the modal input layer, so components behind the modal do not process the same keys. For hierarchical data, see TreeSelect.