4.3k

List

A vertical stack of items. The rows are list-item text leaves — the label is the content, icon draws a leading vector icon, selected marks the current row (usually an equality), disabled grays a row out, and on-press dispatches the row's Msg. For long data, virtualized with a fixed virtual-item-extent lays out only the visible window.

list
A list with icons, a selected row, and a disabled row rendered by the engine (light theme)
icon rows with one selected and one disabled item

Markup

<list width="340" gap="2">
  <list-item icon="file-text" selected="{doc == report}" on-press="open_report">Quarterly report.md</list-item>
  <list-item icon="file-text" selected="{doc == checklist}" on-press="open_checklist">Launch checklist.md</list-item>
  <list-item icon="folder" on-press="open_archive">Archive</list-item>
  <list-item icon="music" disabled="true">demo-track.wav</list-item>
</list>

Zig builder

ui.list(.{ .width = 340, .gap = 2 }, .{
    ui.listItem(.{ .icon = "file-text", .selected = model.doc == .report, .on_press = .open_report }, "Quarterly report.md"),
    ui.listItem(.{ .icon = "file-text", .selected = model.doc == .checklist, .on_press = .open_checklist }, "Launch checklist.md"),
    ui.listItem(.{ .icon = "folder", .on_press = .open_archive }, "Archive"),
    ui.listItem(.{ .icon = "music", .disabled = true }, "demo-track.wav"),
})

Selection and activation

When selection is model state — the accent row your app owns — rows use the desktop gesture split: a single click selects (on-press), and the primary action (open the record, play the track) rides the double click and Enter. Bind the primary action twice — on_double_press for the pointer (Zig builder only; markup has no double-click event) and on-submit for the keyboard:

ui.listItem(.{
    .selected = model.selected == track.id,
    .on_press = Msg{ .select_track = track.id },      // click, and Space on a ring-focused row
    .on_double_press = Msg{ .play_track = track.id }, // double click: the primary action
    .on_submit = Msg{ .play_track = track.id },       // plain Enter: the same primary action
}, track.title)

The double click is additive, never a delay: the first click dispatches the select on its own release, the second release dispatches the play — select-then-act, with no press timer. On the keyboard, a bound on-submit makes plain Enter the row's primary action while Space keeps select; rows without one resolve Enter as select, unchanged.

The arrows stay app-owned after clicking around: pointer focus on a plain list row is quiet (no ring), and a quietly focused row routes no keys — arrows and Enter fall through to the app-level key fallback, where a selection-owning app moves its own selection. Tab onto a row draws the ring and restores the row's full keymap (arrows walk the rows, Space selects, Enter submits), and rows carrying role="treeitem" keep the tree's roving keymap under either register. The full routing order is in Native UI § Keyboard routing; examples/soundboard's track lists are the live reference for the whole pattern.

Virtualization

For long row sets, turn on virtualized and give each row a fixed virtual-item-extent — the engine lays out only the rows in view. Loop the model data with a keyed for and dispatch a payload per row. The rows still all BUILD (this bounds layout and paint, not the tree), so it suits row sets the model already holds — hundreds, not hundreds of thousands. For dataset-scale rows where the view should only ever build the visible window, use the builder's virtual list.

<list virtualized="true" virtual-item-extent="28" grow="1">
  <for each="rows" as="row" key="id">
    <list-item on-press="open_row:{row.id}">{row.title}</list-item>
  </for>
</list>

Attributes

List:

AttributeDescription
virtualizedEnable list virtualization (true/false).
virtual-item-extentFixed item extent for virtualized lists (plain number).
gapSpacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow.

List-item:

AttributeDescription
textText value for text-bearing elements; a literal or one {binding}.
iconbutton, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known_icon_names, e.g. save, plus, refresh-cw), an app-registered app:<name>, or one {binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint.
selectedSelected state; often a {a == b} equality.
disabledDisables the control; true/false or a {binding}.
on-pressDispatch a Msg on press: tag or tag:{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).
on-submitDispatch a Msg on submit: tag or tag:{payload}. Enter in a text field, primary+enter in a textarea; on a list-item, plain Enter dispatches it as the row's primary action while Space keeps the row's select (on-press).