ListWidget
A selectable list of text items with keyboard and mouse navigation.
Lists are interactive, focusable widgets that allow users to navigate through items and select or activate them. They're ideal for menus, file browsers, todo lists, and any UI requiring item selection.
Basic Usage
Create a list using the fluent API with an array of strings:
using Hex1b;
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Border(b => [
b.VStack(v => [
v.Text("Select a fruit:"),
v.Text(""),
v.List(["Apple", "Banana", "Cherry", "Date", "Elderberry"])
])
], title: "Fruit List"))
.Build();
await terminal.RunAsync();dotnet runNavigation
Use Up/Down arrows to move between items. The selected item is highlighted when the list has focus. Press Tab to move focus to other widgets.
Selection Changed Events
The OnSelectionChanged event fires whenever the user navigates to a different item:
using Hex1b;
var state = new ListSelectionState();
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Border(b => [
b.VStack(v => [
v.Text($"Selected: {state.SelectedItem ?? "None"}"),
v.Text(""),
v.List(["Apple", "Banana", "Cherry", "Date", "Elderberry"])
.OnSelectionChanged(e => state.SelectedItem = e.SelectedText)
])
], title: "Selection Demo"))
.Build();
await terminal.RunAsync();
class ListSelectionState
{
public string? SelectedItem { get; set; }
}dotnet runThe ListSelectionChangedEventArgs provides:
SelectedIndex- The index of the newly selected itemSelectedText- The text of the newly selected itemWidget- The source ListWidgetNode- The underlying ListNodeContext- Access to the application context
Selection vs Activation
OnSelectionChanged fires when navigating with arrow keys. Use OnItemActivated (shown below) to respond to Enter/Space/Click actions.
Item Activated Events
The OnItemActivated event fires when the user activates an item with Enter, Space, or mouse click:
using Hex1b;
var state = new TodoState();
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Border(b => [
b.VStack(v => [
v.Text("Press Enter or Space to toggle items:"),
v.Text(""),
v.List(state.GetFormattedItems())
.OnItemActivated(e => state.ToggleItem(e.ActivatedIndex))
])
], title: "Todo List"))
.Build();
await terminal.RunAsync();
class TodoState
{
private readonly List<(string Text, bool Done)> _items =
[
("Learn Hex1b", true),
("Build a TUI", false),
("Deploy to production", false)
];
public IReadOnlyList<string> GetFormattedItems() =>
_items.Select(i => $"[{(i.Done ? "✓" : " ")}] {i.Text}").ToList();
public void ToggleItem(int index)
{
if (index >= 0 && index < _items.Count)
{
var item = _items[index];
_items[index] = (item.Text, !item.Done);
}
}
}dotnet runThe ListItemActivatedEventArgs provides:
ActivatedIndex- The index of the activated itemActivatedText- The text of the activated itemWidget- The source ListWidgetNode- The underlying ListNodeContext- Access to the application context
Common Pattern
Use OnItemActivated for actions like toggling checkboxes, opening details, or navigating to a new screen. Use OnSelectionChanged for updating dependent UI like preview panes.
Event Handlers
Both OnSelectionChanged and OnItemActivated accept synchronous and asynchronous handlers:
Synchronous Handler
v.List(items).OnItemActivated(e => {
Console.WriteLine($"Activated: {e.ActivatedText}");
})2
3
Asynchronous Handler
v.List(items).OnItemActivated(async e => {
await SaveSelectionAsync(e.ActivatedIndex);
})2
3
Render Loop Blocking
Async handlers block the render loop while awaiting. For long operations, use the background work pattern shown in the ButtonWidget documentation.
Keyboard Navigation
Lists support comprehensive keyboard navigation:
| Key | Action |
|---|---|
| Up Arrow | Move to previous item (wraps to last) |
| Down Arrow | Move to next item (wraps to first) |
| Enter | Activate current item |
| Space | Activate current item |
| Tab | Move focus to next widget |
| Shift+Tab | Move focus to previous widget |
Wrap-Around Navigation
When reaching the first item, pressing Up wraps to the last item. Similarly, pressing Down at the last item wraps to the first.
Mouse Support
Lists support mouse interaction in terminals that support mouse events:
- Left Click - Selects the clicked item (fires
OnSelectionChangedif selection changed) and then activates it (firesOnItemActivated) - Mouse Wheel Up - Moves selection to the previous item
- Mouse Wheel Down - Moves selection to the next item
Long Lists with Scrolling
When a list has more items than can fit in its container, it automatically becomes scrollable. The viewport scrolls to keep the selected item visible as you navigate:
using Hex1b;
// Generate a list of 50 countries
var countries = new List<string>
{
"Argentina", "Australia", "Austria", "Belgium", "Brazil",
"Canada", "Chile", "China", "Colombia", "Czech Republic",
"Denmark", "Egypt", "Finland", "France", "Germany",
"Greece", "Hungary", "India", "Indonesia", "Ireland",
"Israel", "Italy", "Japan", "Kenya", "Malaysia",
"Mexico", "Netherlands", "New Zealand", "Nigeria", "Norway",
"Pakistan", "Peru", "Philippines", "Poland", "Portugal",
"Romania", "Russia", "Saudi Arabia", "Singapore", "South Africa",
"South Korea", "Spain", "Sweden", "Switzerland", "Thailand",
"Turkey", "Ukraine", "United Kingdom", "United States", "Vietnam"
};
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Border(b => [
b.VStack(v => [
v.Text("Select a country (scroll with arrow keys or mouse wheel):"),
v.Text(""),
v.List(countries).FixedHeight(10)
])
], title: "Country Selector"))
.Build();
await terminal.RunAsync();dotnet runUse the .FixedHeight() extension to constrain the list to a specific number of rows. The list will:
- Show only the visible items within the viewport
- Automatically scroll when navigating beyond visible bounds
- Keep the selected item centered when possible
- Support both keyboard and mouse wheel scrolling
Performance
Long lists are efficient because only visible items are rendered. You can safely use lists with hundreds of items without performance concerns.
Focus Behavior
Lists visually indicate their focus and selection state:
| State | Appearance |
|---|---|
| Unfocused, item selected | > Item (indicator only) |
| Focused, item selected | > Item (highlighted with theme colors) |
| Unfocused, item not selected | Item (no indicator) |
When a list loses focus, the selected item still shows the selection indicator but without the highlighting. This helps users track which item was last selected.
Theming
Customize list appearance using theme elements:
var theme = Hex1bTheme.Create()
.Set(ListTheme.SelectedForegroundColor, Hex1bColor.Black)
.Set(ListTheme.SelectedBackgroundColor, Hex1bColor.Cyan)
.Set(ListTheme.SelectedIndicator, "▶ ")
.Set(ListTheme.UnselectedIndicator, " ");
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) =>
{
options.Theme = theme;
return ctx => /* ... */;
})
.Build();
await terminal.RunAsync();2
3
4
5
6
7
8
9
10
11
12
13
14
15
Available Theme Elements
| Element | Type | Default | Description |
|---|---|---|---|
ForegroundColor | Hex1bColor | Default | Text color for unselected items |
BackgroundColor | Hex1bColor | Default | Background for unselected items |
SelectedForegroundColor | Hex1bColor | White | Text color when focused and selected |
SelectedBackgroundColor | Hex1bColor | Blue | Background when focused and selected |
SelectedIndicator | string | "> " | Indicator for selected items |
UnselectedIndicator | string | " " | Indicator for unselected items |
Custom Indicators
Change the selection indicators to match your app's style. Common alternatives include "→ ", "● ", "▸ ", or emoji like "👉 ".
State Management
Lists preserve their selection state across reconciliation. The selected index is managed internally by the ListNode and persists even when the widget tree is rebuilt.
When the items list changes, the selection is automatically clamped to valid bounds:
- If the selected index is beyond the new list length, it moves to the last item
- If the list becomes empty, the selection resets to index 0
Typed Lists and Item Templates
For Spectre-style selection prompts and other cases where each row needs a custom multi-line layout, use TypedList<T> with ItemTemplate. The list items can be any type (records, classes, primitives), and each row is rendered as a widget tree returned by your template.
using Hex1b;
var countries = new[]
{
new Country("Australia", "Canberra", "🇦🇺"),
new Country("Brazil", "Brasilia", "🇧🇷"),
new Country("Japan", "Tokyo", "🇯🇵"),
new Country("Norway", "Oslo", "🇳🇴"),
new Country("Portugal", "Lisbon", "🇵🇹"),
};
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Border(b => [
b.TypedList(countries)
.ItemHeight(2)
.ItemKey(c => c.Name)
.ItemTemplate(context =>
{
var prefix = context.IsSelected ? "▶ " : " ";
return context.VStack(v => [
v.Text($"{prefix}{context.Item.Flag} {context.Item.Name}"),
v.Text($" {context.Item.Capital}")
]);
})
], title: "Pick a Country"))
.Build();
await terminal.RunAsync();
record Country(string Name, string Capital, string Flag);dotnet runTypedList<T>(items) returns a TypedListWidget<T> that supports the same keyboard navigation, mouse handling, and selection model as List, plus:
| Method | Purpose |
|---|---|
.ItemTemplate(context => …) | Custom per-row widget tree. The list draws no selector or background — the template owns all chrome. |
.ItemHeight(rows) | Fixed row height in terminal rows. Required when the template is multi-line. Defaults to 1. |
.ItemKey(item => …) | Stable key per item so template subtrees survive reorder/filter. Recommended whenever items can move. |
.InitialSelectedIndex(n) | Index selected when the list is first created. |
.OnSelectionChanged(args => …) | Fires when the user navigates to a different row. args.SelectedItem is typed as T. |
.OnItemActivated(args => …) | Fires on Enter/Space/Click. args.ActivatedItem is typed as T. |
Inside the template callback, the context parameter exposes:
| Member | Description |
|---|---|
Item | The typed item value (T). |
Index | The item's zero-based position. |
IsSelected | true for the currently selected row. |
IsFocused | true when the list itself has focus. |
IsHovered | true for the row currently under the mouse cursor. |
Because ListItemContext<T> derives from WidgetContext<TypedListWidget<T>>, you can call any widget extension (context.Text(...), context.VStack(...), context.Border(...), etc.) from inside the template.
Default rendering
If you don't set ItemTemplate, TypedList<T> renders each row as a single line of item?.ToString() — visually identical to the basic List widget. This means you can switch any List(strings) callsite to TypedList(myObjects) without writing a template, and override object.ToString() (or use record types) to control the row text.
Focusable widgets inside templates
In the current release, interactive widgets placed inside an item template (buttons, text boxes, nested lists) are rendered but do not receive focus — the list itself is the only focusable surface. Use OnItemActivated to drive per-row actions.
Related Widgets
- ButtonWidget - For single-action controls
- TextBoxWidget - For text input with selection
- ScrollPanelWidget - For scrollable content areas
- VStackWidget - For vertical layout of widgets