Combobox

A searchable dropdown for selecting one or multiple values from a list.

#Select vs Combobox

FeatureSelectCombobox
Searchable✓ (optional)✓ (optional)
Multi-select—✓ (.multiple(true))
Custom trigger rendering—✓
Custom item rendering—✓
Footer action slot—✓

Use Select for simple single-value picking. Use Combobox when you need multi-select, a fully custom trigger, or custom item rendering.

#Import

use gpui_kit::component::combobox::{
    Combobox, ComboboxState, ComboboxEvent, ComboboxTriggerCtx,
};
use gpui_kit::component::searchable_list::{
    SearchableListItem, SearchableVec, SearchableGroup,
};

#Usage

#Basic Single-Select

let state = cx.new(|cx| {
    ComboboxState::new(
        SearchableVec::new(vec!["Next.js", "SvelteKit", "Nuxt.js"]),
        vec![], // no initial selection
        window,
        cx,
    )
    .searchable(true)
});

Combobox::new(&state)
    .placeholder("Select framework...")
    .search_placeholder("Search...")
    .w_full()

#Multi-Select

Pass .multiple(true) to enable multi-select mode. Clicking an item toggles it; the dropdown stays open until the user presses Escape or clicks outside.

let state = cx.new(|cx| {
    ComboboxState::new(
        SearchableVec::new(vec!["React", "Vue", "Angular"]),
        vec![IndexPath::new(0)], // pre-selected
        window,
        cx,
    )
    .multiple(true)
    .searchable(true)
});

Combobox::new(&state).placeholder("Select frameworks")

#Pre-selected Item

Pass index paths of items to pre-select:

let state = cx.new(|cx| {
    ComboboxState::new(items, vec![IndexPath::new(0)], window, cx)
});

#Grouped Items

Use SearchableGroup to group items under a heading:

let grouped = SearchableVec::new(vec![
    SearchableGroup::new("Fruits").items(vec![
        FoodItem::new("Apples"),
        FoodItem::new("Bananas"),
    ]),
    SearchableGroup::new("Vegetables").items(vec![
        FoodItem::new("Carrots"),
        FoodItem::new("Spinach"),
    ]),
]);

let state = cx.new(|cx| {
    ComboboxState::new(grouped, vec![], window, cx).searchable(true)
});

Combobox::new(&state)

#Implementing SearchableListItem

Built-in implementations exist for String, SharedString, and &'static str. For custom types implement the trait:

#[derive(Clone)]
struct Country {
    name: SharedString,
    code: SharedString,
}

impl SearchableListItem for Country {
    type Value = SharedString;

    fn title(&self) -> SharedString {
        self.name.clone()
    }

    fn value(&self) -> &SharedString {
        &self.code
    }

    fn matches(&self, query: &str) -> bool {
        self.name.to_lowercase().contains(query)
            || self.code.to_lowercase().contains(query)
    }
}

#Disabled Items

Return true from disabled() on items that should not be selectable:

impl SearchableListItem for MyItem {
    // ...
    fn disabled(&self) -> bool {
        self.is_unavailable
    }
}

#Custom Check Icon

Combobox::new(&state)
    .check_icon(Icon::new(IconName::CircleCheck))

Render a persistent action at the bottom of the dropdown (e.g. an “Add new” button):

Combobox::new(&state)
    .footer(|_, cx| {
        Button::new("add-new")
            .ghost()
            .label("New item")
            .icon(Icon::new(IconName::Plus))
            .w_full()
            .justify_start()
            .into_any_element()
    })

#Custom Trigger

Override the entire trigger element. ComboboxTriggerCtx exposes the current selection, open/disabled flags, and size:

Combobox::new(&state)
    .render_trigger(|ctx, _, cx| {
        h_flex()
            .w_full()
            .items_center()
            .gap_2()
            .when(ctx.selection.is_empty(), |this| {
                this.text_color(cx.theme().muted_foreground)
                    .child("Select...")
            })
            .children(ctx.selection.iter().map(|(_, item)| {
                div()
                    .bg(cx.theme().accent)
                    .rounded_sm()
                    .px_1p5()
                    .py_0p5()
                    .text_sm()
                    .child(item.title())
            }))
            .into_any_element()
    })

#Sizes

Combobox::new(&state).large()
Combobox::new(&state)  // medium (default)
Combobox::new(&state).small()

#Cleanable

Combobox::new(&state).cleanable(true) // show clear button when a value is selected

#Disabled

Combobox::new(&state).disabled(true)

#Events

Both Change (fired on every toggle) and Confirm (fired when the dropdown closes) carry the full selection as Vec<Value>.

cx.subscribe_in(&state, window, |view, _, event, window, cx| {
    match event {
        ComboboxEvent::Change(values) => {
            // fired on every toggle
        }
        ComboboxEvent::Confirm(values) => {
            // fired when the dropdown closes
        }
    }
});

#Mutating Programmatically

Values are resolved through the current delegate. Values that cannot be found are ignored.

set_selected_values clears the search query first, so an active search never decides which values can be selected. Index paths address the list as it is currently displayed, so set_selected_indices, add_selected_index and remove_selected_index act on the visible rows and leave the query alone.

// Replace the entire selection by value
state.update(cx, |s, cx| {
    s.set_selected_values(&["React", "Angular"], window, cx);
});

// Replace the entire selection by index path
state.update(cx, |s, cx| {
    s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx);
});

// Add / remove individual items
state.update(cx, |s, cx| {
    s.add_selected_index(IndexPath::new(1), cx);
    s.remove_selected_index(IndexPath::new(0), cx);
});

// Clear all selections
state.update(cx, |s, cx| {
    s.clear_selection(cx);
});

// Read all selected values (multi-select)
let values = state.read(cx).selected_values(); // Vec<Value>

// Read the first selected value (single-select convenience)
let value = state.read(cx).selected_value(); // Option<Value>

#Keyboard Shortcuts

KeyAction
TabFocus trigger
EnterOpen menu or confirm highlighted item
Up/DownNavigate options (opens menu if closed)
EscapeClose menu

#Theming

  • background — Dropdown input background
  • input — Trigger border color
  • foreground — Text color
  • muted_foreground — Placeholder and disabled text
  • border — Menu border
  • radius — Border radius

CC BY 4.0 · Documentation prose and original illustrations GPUI Kit has rights to license may be copied or adapted with attribution. License. Credit GPUI Kit, link to this source page and the CC BY 4.0 license, and indicate any changes. Code examples and software source remain under Apache-2.0. Existing Apache permissions remain; third-party material retains its own terms.