Select
A dropdown for choosing one option. Its panel opens in a popup layer, so it overlays surrounding content and also works inside dialogs.
use ratcn::{ListItem, Select};
let select = Select::new([
ListItem::new(Fruit::Mango, "Mango"),
ListItem::new(Fruit::Papaya, "Papaya"),
])
.placeholder("Pick a fruit...")
.open(|s: &AppState| s.open, Msg::OpenChanged)
.item_focus(|s: &AppState| s.cursor, Msg::Focused)
.selection(|s: &AppState| s.selected, Msg::Selected);
ctx.render_component("fruit", select, area);Options use the same value-keyed ListItem as List, so reordering them does not change the selected value.
State
The app owns three values: whether the panel is open, the option cursor, and the committed selection. A selection update should store the choice, align the cursor, and close the panel in one message:
Msg::Selected(fruit) => {
state.selected = Some(fruit);
state.cursor = Some(fruit);
state.open = false;
}Interaction
Enter, Space, Up, or Down opens a focused Select. While open, arrow, Home, End, and Page keys move the cursor; Enter or Space selects; Esc closes. The first Tab closes the panel, and the next Tab moves focus.
Pointer motion moves the cursor, and a left click selects the option under the pointer, including the first option where it overlays the trigger row. Pressing outside dismisses the panel while leaving the underlying control clickable.
The panel shows at most eight options by default and scrolls to keep the cursor visible. .max_visible_options(...) changes that limit. Its top border starts one row above the trigger, so the first option covers the trigger row. The panel stays fixed while the cursor moves, shifting only when needed to remain inside the frame.
Disabled
ListItem::disabled(true) dims one option and skips it for keys and clicks. .disabled(true) disables the whole Select and removes it from Tab traversal.
ListItem::new(Fruit::Durian, "Durian").disabled(!state.durian_available)Styling
The trigger's default, focus, and hover backdrops match List. Hover is slightly lighter than focus, so it remains visible when the trigger already has keyboard focus. SelectStyle controls trigger, panel, cursor, selection, and disabled colors. SelectWidget is the paint-only ratatui widget for using the same appearance without the runtime.
Override one Select with .style(...). The closure receives the active theme each render, so a derived style follows theme switches:
use ratcn::SelectStyle;
Select::new(items).style(|theme| {
let mut style = SelectStyle::from_theme(theme);
style.selected_marker = theme.accent;
style
})SelectStyle::fallback() is the no-theme starting point: plain ANSI colors that render on any terminal.
Paint-Only Widget
SelectWidget draws a Select without focus or events. It is an ordinary Ratatui widget, so it works in a plain Ratatui app with no Ratcn runtime. Options and state are addressed by index:
use ratcn::SelectWidget;
let options = ["Mango", "Papaya", "Lychee", "Durian"];
frame.render_widget(
SelectWidget::new(selected_label)
.placeholder("Pick a fruit...")
.open(&options)
.focused_option(Some(cursor_index))
.selected_option(selected_index)
.disabled_options(&[false, false, true, false])
.scroll_offset(scroll_offset)
.focused(select_has_focus)
.hovered(pointer_is_over_select)
.disabled(select_is_disabled)
.themed(&theme),
area,
);Use SelectWidget::height(...) and SelectWidget::visible_options(...) when the surrounding layout needs to reserve exactly the rows the open widget will paint. Replace .themed(...) with .style(...) to supply exact widget colors.
Notes
- Option values must be unique within a Select; duplicates panic during declaration.
- An empty Select, or one whose options are all disabled, is skipped by Tab.
- Mouse input needs capture enabled in the host — see Mouse input.
Full API
Every method, with binding requirements and edge-case detail: Select, SelectWidget, SelectStyle, ListItem.
See Also
Use List when several options should remain visible instead of opening from a trigger.