VirtualListFlow

Same external API as VirtualList, but items render in CSS normal flow instead of being taken out of flow and positioned individually. Use this variant when virtualization needs to coexist with position: sticky inside items, CSS gap, or any other layout rule that depends on items participating in the document's flow.

Why a second variant

In the original VirtualList, items don't participate in the document's normal flow, so layout rules that depend on flow behave differently: position: sticky inside an item pins to that item rather than the outer scroll container, and CSS gap / margin between items has to be baked into each item's own padding.

VirtualListFlow keeps the same windowing behavior but renders items in normal flow, so sticky, gap, and browser scroll anchoring work the way they do in a non-virtual list. The trade-off is a slightly heavier repositioning cost on scroll — usually well worth it for lists of cards or sections with layered structure inside them.

When to pick which

VirtualList (absolute)VirtualListFlow (flow)
Sticky inside an itemPinned to item, not scroll containerPinned to scroll container ✓
CSS gap / margin between itemsBake into item paddingWorks natively ✓
Compositing layersOne per visible itemNone per item ✓
Repositioning cost on scrollCompositor only (cheap)Layout (cheap, but heavier than compositor)
Async height-change scroll driftPossibleBrowser scroll anchoring keeps stable ✓

If a list doesn't need any of the flow-side wins, VirtualList is fine and slightly cheaper. For any list of cards, sections, or items with sticky/layered structure inside them, prefer VirtualListFlow.

Fixed height

Pass itemHeight as a number (px).

Item 1
Item 2
Item 3
Item 4

Dynamic height

Pass itemHeight="dynamic" for content-driven heights. Each item is measured after it renders. Use estimatedItemHeight to set the initial layout estimate; the scroll position stays stable when measured heights replace estimates.

Item 1 — This item has a longer description that wraps onto multiple lines to demonstrate dynamic height measurement.
Item 2
Item 3
Item 4

Infinite scroll

Pass hasMore, loadingMore, and onLoadMore. The callback fires when the scroll position is within loadMoreThreshold px (default 80) of the bottom.

Showing 30 / 200

Item 1
Item 2
Item 3
Item 4

Props

VirtualListFlow re-exports the same VirtualListProps type as VirtualList, so callers can swap one for the other without rewriting JSX. The style argument passed to the children render fn is just { width: '100%' } (plus height in fixed mode) — no positioning information, because items are in normal flow and don't need any.

PropTypeDefaultDescription
itemsT[]Array of items to render.
itemHeightnumber | "dynamic"Fixed height in px, or "dynamic" for content-driven heights.
estimatedItemHeightnumber40Initial height estimate used in dynamic mode.
heightnumber | string"100%"Container height.
overscannumber3Extra items rendered outside the visible area.
getItemKey(item, index) => string | numberindexReturns a stable key for each item.
children(item, index, style) => ReactNodeRender function. Applying style is optional in flow mode — only width (and height in fixed mode) are set.
hasMorebooleanfalseWhether more items can be loaded.
loadingMorebooleanfalseShows a loading indicator at the bottom while true.
onLoadMore() => voidCalled when scroll reaches the load-more threshold.
loadMoreThresholdnumber80Px from bottom that triggers onLoadMore.
renderLoadingMoreReactNode"Loading more…"Custom content shown while loadingMore is true.
onScroll(scrollTop: number) => voidFires on scroll with the current scrollTop value.
onRangeChange(start, end) => voidFires when the visible index range changes.
stickyHeaderReactNodeHeader rendered inside the scroll container with position: sticky.
stickyHeaderHeightnumber0Height of stickyHeader. Subtracted from visible-area calculations so items don't render under it.
classNamestringExtra classes on the container.