Virtualized Lists and Grids
Lui.VirtualList and Lui.VirtualGrid render large collections by mounting only the lines visible in the viewport (plus a small overscan buffer) and recycling element instances as you scroll. Cost is proportional to the visible window, not the dataset, so a 100,000-item list mounts the same handful of elements as a 100-item one. Row/cell height is measured automatically, so items may vary in height.
Lui.VirtualList
Lui.VirtualList(
items, // IReadOnlyList<T>
(item, index) => Row(item), // item renderer -> a single LuiNode
"h-[480px] bg-slate-950 rounded-xl",
key: item => item.Id, // stable identity per item
estimatedItemHeight: 64f)A leaderboard over any number of entries:
Lui.VirtualList(entries, (e, _) =>
Lui.Div("w-full px-4 py-3 flex flex-row items-center gap-3",
Lui.Text("#" + e.Rank, "w-14 text-slate-500 font-bold"),
Lui.Text(e.Name, "grow text-white font-semibold"),
Lui.Text(e.Score.ToString("n0"), "text-emerald-400 font-bold")
),
"h-[480px] bg-slate-900 rounded-xl border border-slate-800",
key: e => e.Id,
estimatedItemHeight: 56f)Lui.VirtualGrid
A responsive inventory/shop grid. cellWidth derives the column count from the viewport width; pass columns to fix it instead. Each row’s height is measured (uniform column width, variable row height).
Lui.VirtualGrid(
items,
cellWidth: 220f, // column count = floor(viewportWidth / cellWidth)
(item, index) => Cell(item),
"h-[480px] bg-slate-900 rounded-xl border border-slate-800 p-1",
columns: 0, // 0 = auto from cellWidth; > 0 = fixed
key: item => item.Id,
estimatedRowHeight: 150f)Requirements
- Bounded height. The control must have a fixed height (
h-[480px],h-96, …) or fill a bounded flex parent (grow min-h-0inside aflex flex-colwhose own height is bounded). Inside another scroll view or any unbounded-height parent, give it an explicit height — otherwise it grows to fit all content and nothing is virtualized. - Stable keys. Pass
keyso an element follows its item across scrolling and data edits (insert/remove/reorder). Without it, lines fall back to index keys and recycle less cleanly. - Single node per item. The renderer must return one element node, not a
Lui.Fragment. Wrap multiple children in aLui.Div. - Estimate.
estimatedItemHeight/estimatedRowHeightseeds the scrollbar before lines are measured; a value near the real average keeps the initial scrollbar accurate. It does not need to be exact.
Reusable item renderer pattern
Define the item renderer as a static method so the delegate and closures are not reallocated each render, and keep item markup shape-stable so recycled elements patch in place instead of being rebuilt:
private static LuiNode SlotCell(SaveSlot slot) =>
Lui.Div("m-1 p-3 rounded-lg bg-slate-800 flex flex-col gap-2",
Lui.Text(slot.Title, "text-white font-semibold"),
Lui.Text(slot.Timestamp, "text-xs text-slate-400"));
// in Render():
Lui.VirtualGrid(slots, 200f, (slot, _) => SlotCell(slot), "h-[420px]", key: s => s.Id);Notes: scrolling updates the visible window directly without re-running Render(). Both controls scroll vertically. See ReadyVirtualizedComponent (the Virtualized tab in the components showcase) for a 10,000-item list and grid with variable height.