Skip to content

Select

Select<T> chooses one value from structured options or a lazy data source while conserving vertical space. T requires Clone + Eq + Hash + 'static. It is strongly typed and controlled: selected_value(Option<T>) is the host-owned authoritative value, while on_change(T, ...) only requests the next value.

Use RadioGroup when a small set of mutually exclusive choices should stay visible. Select has no search, text editing, or multiple selection; those belong to the future Combobox roadmap and MultiSelect roadmap.

Basic controlled usage

Loading component preview...
Open standalone

The host may accept a request immediately or wait for server approval before updating selected_value. If rejected, keep passing the old value; Select does not retain a second business selection. Submitting the authoritative value again emits no redundant change.

Groups and disabled options

Loading component preview...
Open standalone

Group labels provide visible and accessible grouping only. They never enter active, selection, or keyboard indexing. Disabled options remain visible but cannot become active or request a value.

Loading, empty, and error

Loading component preview...
Open standalone

SelectStatus is mutually exclusive. Ready shows options, while Loading, Empty, and Error show host-supplied text. Select never starts a request, waits, retries, or changes status itself. Status content is not an option and cannot emit selection. A non-Ready popup can still open by keyboard or pointer so its status message remains reachable, but arrows, paging, typeahead, and Enter submission cannot establish an active option or request a value.

Keyboard navigation

Loading component preview...
Open standalone
KeyClosedOpen
Enter / SpaceOpen and activate the enabled selected option or first optionSubmit an enabled active option and close
ArrowDownOpen at the enabled selected option or first enabled optionMove to the next enabled option without wrapping
ArrowUpOpen at the enabled selected option or last enabled optionMove to the previous enabled option without wrapping
Home / EndPropagate normallyMove to the first / last enabled option
PageUp / PageDownPropagate normallyMove by the popup's currently measured visible page and clamp at the ends
Printable textIn Ready, open and cycle from after the current option by accessible nameAccumulate a short-lived prefix; repeated characters cycle options with that initial
EscapePropagate normallyClose without changing value; focus stays on the trigger
Tab / Shift+TabTraverse normallyClose and continue normal focus traversal

Unsupported modifiers and unknown keys propagate. Typeahead considers enabled canonical options only, applies a Unicode case-insensitive prefix match, and clears its buffer after a short pause; no match preserves the current active option. Enter and Space use GPUI's complete KeyDown/KeyUp activation cycle; one interaction emits at most one value request.

Million-item lazy data, narrow windows, and resize

Loading component preview...
Open standalone

The popup uses a fixed-height VirtualList and creates only visible option/group rows. An external million-item source receives no full Vektra catalog, HashSet, or Element tree. Short lists shrink to row count × fixed row height and scroll only after reaching the maximum height. Arrow, Home, End, PageUp, PageDown, typeahead, and active reveal call source indexes and do not depend on the target row being rendered. The popup opens downward when its actual content fits, otherwise flips as needed, and remains constrained to the viewport.

cargo run --example select keeps normal and explicitly labeled million-item generated scenarios in the same Select example entry, including visible range, item-read count, and the zero-row cache limit.

Anatomy

text
Select trigger (ComboBox, real Tab stop, expanded)
└─ current label / placeholder + ChevronDown / ChevronUp indicator
Select popup (ListBox, private viewport-constrained overlay)
└─ VirtualList + Vektra Scrollbar
   ├─ SelectGroup (Group)
   │  ├─ group label (Label)
   │  └─ SelectOption (ListBoxOption)
   └─ loading / empty (Status) or error (Alert)

Selection uses a trailing Check icon while the active option uses a subtle background. The error ! and focus-visible outline also provide non-color cues.

API

APIDescription
Select::new(id)Creates an unselected Select with a stable root ElementId.
.selected_value(Option<T>)Supplies the host-owned authoritative value.
.option(SelectOption<T>)Adds a top-level structured option.
.group(SelectGroup<T>)Adds a titled structured option group.
.items(Vec/array)Adds owned options through the same lazy kernel.
.data_source(Rc<dyn SelectDataSource<T>>)Uses a generated, paged, or remote lazy source.
.placeholder(text)Sets trigger text when no valid selection exists; defaults to “请选择”.
.status(SelectStatus)Sets Ready, Loading, Empty, or Error.
.on_change / .on_change_inRequests the next value without optimistic selection.
.disabled(bool)Disables the trigger and removes it from normal Tab order.
.size(ComponentSize)Applies Xs, Sm, Md, or Lg.
.on_focus / .on_blur and _inObserves real trigger focus transitions.
.aria_label / .aria_descriptionSets the trigger's accessible name and description.
SelectOption::new(id, value, label)Creates a stable ID, typed value, and visible label.
.icon(IconSource)Adds an optional decorative icon.
.description(text)Adds visible detail and its fallback accessible description.
.aria_label / .aria_descriptionOverrides option accessibility text.
.disabled(bool)Removes the option from active and submission paths.
SelectGroup::new(id, label)Creates a group with a stable ID and visible heading.
.aria_label(text)Overrides the group's accessible name.
.option(SelectOption<T>)Adds a structured option of the same value type.
OwnedSelectDataSourceAdapts owned options/entries into the unified protocol.
SelectDataSource<T>Supplies count/revision/key/item, key/value lookup, enabled navigation, typeahead, loading state, and range requests.

Select implements Changeable<T>, Disableable, Sizable, and Focusable. SelectOption implements Disableable. Select deliberately does not implement Clickable: opening/closing the trigger and requesting an option are composite selection semantics, not one raw click contract. It also does not implement arbitrary-Element ParentElement.

Stable identity and dynamic options

  • Option IDs and business values should each be unique within a Select; group IDs should also remain stable.
  • Duplicate IDs or values use input-order first-match/canonical behavior. The first canonical option works normally; later conflicts act disabled and cannot create a second selected visual or callback.
  • Removing the selected option shows the placeholder without selecting a replacement or calling on_change.
  • If a selected option becomes disabled, the trigger still shows the authoritative value, but that option cannot become active or submit again.
  • Removing the active option prefers the next enabled option at its old position, then the nearest previous option. Reordering follows stable IDs.
  • An all-disabled popup still opens and closes safely with no active option.

Focus, closing, and accessibility

The trigger is the only real focus target and normal Tab stop. While open, focus stays on it and the active option is reported through GPUI/AccessKit active-descendant semantics. Submitting an enabled option closes and restores trigger focus. Clicking the trigger again, clicking outside, Escape, Tab/Shift+Tab, or window deactivation closes the popup. Internal clicks, wheel input, and Scrollbar interaction are not treated as outside clicks.

The trigger reports ComboBox, name, description, expanded, and disabled. Popup, group, and option report ListBox, Group, and ListBoxOption; options report selected, disabled, and whole-source posinset/setsize. Virtualization exports visible AccessKit children only, and the active option is revealed before materialization. Loading/empty use Status, and error uses Alert.

Disabled, expanded, selected, name, description, and value mappings have deterministic AccessKit node assertions. Roles, active-descendant, and focus paths are covered by locked-GPUI compilation and interaction tests. GPUI's regular test platform does not activate a complete assistive-technology tree, so VoiceOver, NVDA, Narrator, Orca, and platform announcement behavior have not been manually verified.

Themes and cross-platform status

Light, Dark, and System resolve dedicated Select trigger, popup, option, group, status, and Xs/Sm/Md/Lg tokens. The scroll area keeps using shared Scrollbar tokens. No arbitrary color, radius, or spacing overrides are exposed.

Custom themes must now pass complete Select tokens through ResolvedTheme::from_tokens. Missing keys, wrong types, or invalid references return ThemeError; the legacy missing-extension fallback is gone. Migrate by supplying all six trigger states, five option states, and four sizes, then replace string access with infallible select_trigger_state(SelectTriggerState), select_option_state(SelectOptionState), and select_size(ThemeSize) calls.

The code targets GPUI-supported macOS, Windows, Linux, and Web/WASM. Local compilation, deterministic interaction tests, popup constraints under 1.25x/1.5x/2x test scaling, million-item materialization bounds, and the shared WASM build are covered. Dedicated Windows/Linux performance, physical high-DPI, and screen readers remain unverified.

Performance contract

  • The owned adapter performs expected-O(n) canonical validation with a temporary option-ID HashSet plus its final value index, then releases the temporary set; search text is not duplicated.
  • External large sources own uniqueness, key/value lookup, enabled navigation, and typeahead indexes.
  • Popup Element/layout/prepaint/paint/AccessKit is O(visible), with zero overdraw and a hard row-cache limit of zero.
  • The normal benchmark covers complete 10K behavior with a retained, prebuilt owned source and a ≤16.67ms first-open goal; separate 10K/100K benchmarks measure data construction. The stress benchmark uses a 1M lazy source. See Performance Architecture.

Known limitations

  • Single selection and non-editable: no search, filtering, IME, Combobox, or MultiSelect.
  • Options accept a label, optional IconSource, and description, not arbitrary Elements or slots.
  • Status is entirely host-driven; Select owns no async task or retry logic.
  • The popup remains private; VirtualList provides the public fixed-height collection primitive.
  • Owned group/description rows share one fixed maximum row height; no variable-height exact index is provided.
  • Desktop example: cargo run --example select.