Skip to Content
Version 1AdvancedVirtualized Lists and Grids

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-0 inside a flex flex-col whose 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 key so 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 a Lui.Div.
  • Estimate. estimatedItemHeight / estimatedRowHeight seeds 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.

Last updated on