Virtual Multi Select
Virtual Multi Select is not a separate built-in component. It is a recommended composition pattern: VirtualList owns cursor movement, windowed rendering, and the confirm key, while business code stores the multi-select set in an external HashSet<usize>. This keeps long lists, selection state, and row rendering on clear boundaries.

This recording is generated by docs/tapes/virtual-multi-select.tape and runs examples/components/virtual_multi_select.rs. The example waits for 10,000 rows to load, restores the default cursor to row 6, checks multiple rows with Space, and submits the current selection count with Enter.
Press j/k to move, Space to toggle the current row, Enter to submit the selected count, and q to quit.
When to use this pattern
Section titled “When to use this pattern”Prefer MultiSelect for ordinary multi-select. Reach for VirtualList + external selected set only when at least one of these is true:
- The list is long and needs windowed rendering.
- Each row needs a custom widget or complex style.
- The selection set belongs to the business model and must be read across panels, submitted in batches, or persisted.
- Row data comes from an external data source and the component should not own the whole collection.
This pattern is intentionally not captured as a VirtualMultiSelect component. “Selected” in long lists is often tightly bound to business policy: by id, by filtered results, by page, by permission-disabled items. The framework provides composable low-level abilities instead of freezing those policies into a built-in component.
State boundaries
Section titled “State boundaries”The example has three key pieces of state:
loaded controls whether data is ready; list_state is passed to VirtualList for cursor management; selected is the multi-select set owned by business code. VirtualList does not know which rows are checked. It receives a snapshot during render_item and draws visible rows accordingly.
Toggling selection
Section titled “Toggling selection”The multi-select shortcut is registered on the page layer, not inside VirtualList:
Space only updates selected; it does not change VirtualList behavior. Enter submits the selected count here. If you want Enter to still trigger VirtualList(on_select), you can move submit logic into on_select, but make sure the same key is not consumed by two handlers.
Rendering checked state
Section titled “Rendering checked state”The render_item closure uses a snapshot of the selected set to decide how to draw each row:
Cloning a HashSet<usize> here gives render_item a stable snapshot for the current frame. In a real application with a very large selected set, you can switch business selection to HashSet<ItemId> or maintain a lighter lookup structure outside the component. The component API still needs only the current index and visible row context.
Difference from MultiSelect
Section titled “Difference from MultiSelect”| Need | Choose |
|---|---|
| Small to medium list where selected state only serves the current control | MultiSelect |
| Long list that needs windowed rendering | VirtualList + HashSet |
| Fully custom row height, style, or widget | VirtualList + render_item |
| Need to persist selection by business id across pages | External business state + VirtualList |
Next, see VirtualList for the windowed list API itself. If you do not need virtualization, MultiSelect is simpler.