Select
A select component that allows users to choose from a list of options.
Supports search functionality, grouped items, custom rendering, and various states. Built with keyboard navigation and accessibility in mind.
#Import
use gpui_kit::component::select::{
Select, SelectState, SelectItem, SelectDelegate,
SelectEvent, SearchableVec, SelectGroup
};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { SelectItem, Select } from "select.slint";use iced::keyboard;
use iced::widget::{column, container, mouse_area, scrollable, svg, text};
use iced::{Alignment, Element, Length, Subscription, Task, Theme};#Usage
#Framework
// The whole of a select: the items, the caller's chosen index and open flag,
// and the two reports. The trigger is the field's frame — the same height,
// inset and edge as `Input` — and the panel is the popover's one surface
// around the menu's rows. `Item` is the shape the menu and the dropdown
// button already share, so an option can carry an icon or a shortcut.
let framework = body_box(
Select::new(vec![
Item::new("GPUI"),
Item::new("Slint"),
Item::new("egui"),
])
.selected(app.selected[slot::FRAMEWORK])
.open(app.open[slot::FRAMEWORK])
.highlighted(app.highlighted_in(slot::FRAMEWORK))
.placeholder("Choose a framework")
.width(Length::Fixed(TRIGGER_WIDTH))
.on_open_change(|open| Message::Opened {
slot: slot::FRAMEWORK,
open,
})
.on_chosen(|index| Message::Chosen {
slot: slot::FRAMEWORK,
index,
})
.on_highlight(|index| Message::Highlighted {
slot: slot::FRAMEWORK,
index,
})
.on_leave(|row| Message::Left {
slot: slot::FRAMEWORK,
row,
}),
);You can create a basic select dropdown by initializing a SelectState with a list of items.
The first type parameter of SelectState is the items for the state, which must implement the SelectItem trait.
The built-in implementations of SelectItem include common types like String, SharedString, and &'static str.
let state = cx.new(|cx| {
SelectState::new(
vec!["Apple", "Orange", "Banana"],
Some(IndexPath::default()), // Select first item
window,
cx,
)
});
Select::new(&state)Select {
placeholder: "Choose a framework";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
{ label: "egui", value: "egui" }
];
selected: 0;
}#Placeholder
let state = cx.new(|cx| {
SelectState::new(
vec!["Rust", "Go", "JavaScript"],
None, // No initial selection
window,
cx,
)
});
Select::new(&state)
.placeholder("Select a language...")VerticalLayout {
spacing: 12px;
Select {
placeholder: "Choose a framework";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: -1;
}
Select {
placeholder: "No region matches this filter";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: -1;
enabled: false;
}
}// Nothing is selected, so the trigger shows the placeholder; the second
// select is disabled, which is the placeholder card the Slint example shows
// beside the first.
let placeholder = body_box(
column![
select(app, slot::PLACEHOLDER).placeholder("Choose a framework"),
Select::new(items(slot::PLACEHOLDER))
.placeholder("No region matches this filter")
.width(Length::Fixed(TRIGGER_WIDTH))
.disabled(true),
]
.spacing(12.0),
);#Accessibility
Give the control a name that stays the same when the selection changes:
Select::new(&state)
.accessibility_label("Programming language")
.placeholder("Choose a language")The accessible value uses the committed item’s title() and any title_prefix.
A custom display_title() remains visual presentation. Searching does not change
that committed value. With no selection, the accessible value uses the placeholder.
Enabled controls expose accessible activation to open or close the popup.
#Searchable
Use searchable(true) to enable search functionality within the dropdown.
let fruits = SearchableVec::new(vec![
"Apple", "Orange", "Banana", "Grape", "Pineapple",
]);
let state = cx.new(|cx| {
SelectState::new(fruits, None, window, cx).searchable(true)
});
Select::new(&state)
.icon(IconName::Search) // Shows search icon#Impl SelectItem
By default, we have implmemented SelectItem for common types like String, SharedString and &'static str. You can also create your own item types by implementing the SelectItem trait.
This is useful when you want to display complex data structures, and also want get that data type from select_value method.
You can also customize the search logic by overriding the matches method.
#[derive(Debug, Clone)]
struct Country {
name: SharedString,
code: SharedString,
}
impl SelectItem for Country {
type Value = SharedString;
fn title(&self) -> SharedString {
self.name.clone()
}
fn display_title(&self) -> Option<gpui_kit::AnyElement> {
// Custom display for selected item
Some(format!("{} ({})", self.name, self.code).into_any_element())
}
fn value(&self) -> &Self::Value {
&self.code
}
fn matches(&self, query: &str) -> bool {
// Custom search logic
self.name.to_lowercase().contains(&query.to_lowercase()) ||
self.code.to_lowercase().contains(&query.to_lowercase())
}
}#Group Items
let mut grouped_items = SearchableVec::new(vec![]);
// Group countries by first letter
grouped_items.push(
SelectGroup::new("A")
.items(vec![
Country { name: "Australia".into(), code: "AU".into() },
Country { name: "Austria".into(), code: "AT".into() },
])
);
grouped_items.push(
SelectGroup::new("B")
.items(vec![
Country { name: "Brazil".into(), code: "BR".into() },
Country { name: "Belgium".into(), code: "BE".into() },
])
);
let state = cx.new(|cx| {
SelectState::new(grouped_items, None, window, cx)
});
Select::new(&state)#Sizes
Select::new(&state).large()
Select::new(&state) // medium (default)
Select::new(&state).small()Select {
size: ControlSize.small;
placeholder: "Small";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: 0;
}
Select {
placeholder: "Medium";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: 1;
}
Select {
size: ControlSize.large;
placeholder: "Large";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: 0;
}// The size picks the height, the inset, the type and the chevron together,
// the way the Slint component's `size` picks `Theme.input-height`,
// `input-padding-x`, `input-font-size` and `icon-size`.
let sizes = body_box(
column![
select(app, slot::SIZE_SMALL)
.size(theme::ControlSize::Small)
.placeholder("Small"),
select(app, slot::SIZE_MEDIUM).placeholder("Medium"),
select(app, slot::SIZE_LARGE)
.size(theme::ControlSize::Large)
.placeholder("Large"),
]
.spacing(12.0),
);#Disabled State
Select::new(&state).disabled(true)Select {
enabled: false;
placeholder: "Choose a framework";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: 0;
}// A disabled select answers nothing: the trigger is drawn muted and the
// popover is given no open state, so the panel cannot be opened at all.
let disabled = body_box(
Select::new(items(slot::DISABLED))
.selected(app.selected[slot::DISABLED])
.placeholder("Choose a framework")
.width(Length::Fixed(TRIGGER_WIDTH))
.disabled(true),
);#Cleanable
Select::new(&state)
.cleanable(true) // Show clear button when item is selected#Custom Appearance
Select::new(&state)
.w(px(320.)) // Set dropdown width
.menu_width(px(400.)) // Set menu popup width
.menu_max_h(rems(10.)) // Set menu max height (default: 20rem)
.appearance(false) // Remove default styling
.title_prefix("Country: ") // Add prefix to selected titleVerticalLayout {
spacing: 12px;
Select {
title-prefix: "Country: ";
background: Theme.primary.with-alpha(0.08);
border-color: Theme.primary.with-alpha(0.45);
radius: Theme.radius-xl;
menu-width: 360px;
items: [
{ label: "Portugal", value: "pt" },
{ label: "Netherlands", value: "nl" },
{ label: "New Zealand", value: "nz" }
];
selected: 1;
}
Select {
title-prefix: "Build: ";
background: Theme.muted;
border-color: Theme.border;
radius: Theme.radius-sm;
items: [
{ label: "Debug", value: "debug" },
{ label: "Release", value: "release" }
];
selected: 1;
}
// `appearance` off draws no frame at all, for a host that supplies
// the surface itself.
Select {
appearance: false;
placeholder: "Choose a framework";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" }
];
selected: 0;
}
}// The trigger's own surface: a tinted fill, a matching edge and a softer
// radius, a wider panel than the trigger, and a prefix on the value. The last
// one asks for `appearance: false`, for a host that supplies the surface.
let custom = body_box(
column![
select(app, slot::COUNTRY)
.title_prefix("Country: ")
.width(Length::Fixed(220.0))
.menu_width(360.0)
.background(|theme| Tokens::of(theme).primary.scale_alpha(0.08))
.border_color(|theme| Tokens::of(theme).primary.scale_alpha(0.45))
.radius(theme::RADIUS_XL),
select(app, slot::BUILD)
.title_prefix("Build: ")
.background(|theme| Tokens::of(theme).muted)
.border_color(|theme| Tokens::of(theme).border)
.radius(theme::RADIUS_SM),
select(app, slot::PLAIN)
.placeholder("Choose a framework")
.appearance(false),
]
.spacing(12.0),
);#Empty State
let state = cx.new(|cx| {
SelectState::new(Vec::<SharedString>::new(), None, window, cx)
});
Select::new(&state)
.empty(
h_flex()
.h_24()
.justify_center()
.text_color(cx.theme().muted_foreground)
.child("No options available")
)#Events
cx.subscribe_in(&state, window, |view, state, event, window, cx| {
match event {
SelectEvent::Confirm(value) => {
if let Some(selected_value) = value {
println!("Selected: {:?}", selected_value);
} else {
println!("Selection cleared");
}
}
}
});VerticalLayout {
spacing: 8px;
alignment: start;
Select {
placeholder: "Choose a framework";
items: [
{ label: "GPUI", value: "gpui" },
{ label: "Slint", value: "slint" },
];
selected: 0;
changed(index) => {
root.picked = index == 0 ? "GPUI" : "Slint";
}
}
Text {
text: "Last choice: " + picked;
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
}
}// `on_chosen` reports the row by index; the caller owns the items, asks
// itself what index 2 means, and closes the panel by answering `open`. The
// line under the select is the report made visible.
let events = body_box(
column![
select(app, slot::EVENTS).placeholder("Choose a framework"),
muted_line(format!(
"Last choice: {}",
label_of(slot::EVENTS, app.last[slot::EVENTS])
.unwrap_or_else(|| "nothing yet".to_string())
)),
]
.spacing(8.0),
);#Mutating
// Set by index
state.update(cx, |state, cx| {
state.set_selected_index(Some(IndexPath::default().row(2)), window, cx);
});
// Set by value (requires PartialEq on Value type)
state.update(cx, |state, cx| {
state.set_selected_value(&"US".into(), window, cx);
});
// Get current selection
let current_value = state.read(cx).selected_value();Update items:
state.update(cx, |state, cx| {
let new_items = vec!["New Option 1".into(), "New Option 2".into()];
state.set_items(new_items, window, cx);
});#Language Selector
let languages = SearchableVec::new(vec![
"Rust".into(),
"TypeScript".into(),
"Go".into(),
"Python".into(),
"JavaScript".into(),
]);
let state = cx.new(|cx| {
SelectState::new(languages, None, window, cx)
});
Select::new(&state)
.placeholder("Select language...")
.title_prefix("Language: ")#Country/Region Selector
#[derive(Debug, Clone)]
struct Region {
name: SharedString,
code: SharedString,
flag: SharedString,
}
impl SelectItem for Region {
type Value = SharedString;
fn title(&self) -> SharedString {
self.name.clone()
}
fn display_title(&self) -> Option<gpui_kit::AnyElement> {
Some(
h_flex()
.items_center()
.gap_2()
.child(self.flag.clone())
.child(format!("{} ({})", self.name, self.code))
.into_any_element()
)
}
fn value(&self) -> &Self::Value {
&self.code
}
}
let regions = vec![
Region {
name: "United States".into(),
code: "US".into(),
flag: "🇺🇸".into()
},
Region {
name: "Canada".into(),
code: "CA".into(),
flag: "🇨🇦".into()
},
];
let state = cx.new(|cx| {
SelectState::new(regions, None, window, cx)
});
Select::new(&state)
.placeholder("Select country...")#Integrated with Input Field
// Combined country code + phone input
h_flex()
.border_1()
.border_color(cx.theme().input)
.rounded(cx.theme().radius_lg)
.w_full()
.gap_1()
.child(
div().w(px(140.)).child(
Select::new(&country_state)
.appearance(false) // No border/background
.py_2()
.pl_3()
)
)
.child(Separator::vertical())
.child(
div().flex_1().child(
Input::new(&phone_input)
.appearance(false)
.placeholder("Phone number")
.pr_3()
.py_2()
)
)#Multi-level Grouped Select
let mut grouped_countries = SearchableVec::new(vec![]);
for (continent, countries) in countries_by_continent {
grouped_countries.push(
SelectGroup::new(continent)
.items(countries)
);
}
let state = cx.new(|cx| {
SelectState::new(grouped_countries, None, window, cx)
});
Select::new(&state)
.menu_width(px(350.))
.placeholder("Select country...")#Keyboard Shortcuts
// `↑`/`↓` step to the previous and next actionable row, `Home`/`End` jump to
// the first and last, and `Enter` and `Space` choose the one highlighted.
// Escape is the popover's, and closes the panel from anywhere in it.
let keyboard = body_box(
column![
select(app, slot::KEYBOARD).placeholder("Open with the keyboard"),
muted_line("↑ ↓ Home End move the highlight; Enter or Space chooses; Escape closes."),
]
.spacing(8.0),
);| Key | Action |
|---|---|
Tab | Focus dropdown |
Enter | Open menu or select current item |
Up/Down | Navigate options (opens menu if closed) |
Escape | Close menu |
Space | Open menu |
#Theming
The dropdown respects the current theme and uses the following theme tokens:
background- Dropdown input backgroundinput- Border colorforeground- Text colormuted_foreground- Placeholder and disabled textaccent- Selected item backgroundaccent_foreground- Placeholder text colorborder- Menu borderradius- Border radius