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 item | Pinned to item, not scroll container | Pinned to scroll container ✓ |
CSS gap / margin between items | Bake into item padding | Works natively ✓ |
| Compositing layers | One per visible item | None per item ✓ |
| Repositioning cost on scroll | Compositor only (cheap) | Layout (cheap, but heavier than compositor) |
| Async height-change scroll drift | Possible | Browser 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).
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.
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
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.
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Array of items to render. |
itemHeight | number | "dynamic" | — | Fixed height in px, or "dynamic" for content-driven heights. |
estimatedItemHeight | number | 40 | Initial height estimate used in dynamic mode. |
height | number | string | "100%" | Container height. |
overscan | number | 3 | Extra items rendered outside the visible area. |
getItemKey | (item, index) => string | number | index | Returns a stable key for each item. |
children | (item, index, style) => ReactNode | — | Render function. Applying style is optional in flow mode — only width (and height in fixed mode) are set. |
hasMore | boolean | false | Whether more items can be loaded. |
loadingMore | boolean | false | Shows a loading indicator at the bottom while true. |
onLoadMore | () => void | — | Called when scroll reaches the load-more threshold. |
loadMoreThreshold | number | 80 | Px from bottom that triggers onLoadMore. |
renderLoadingMore | ReactNode | "Loading more…" | Custom content shown while loadingMore is true. |
onScroll | (scrollTop: number) => void | — | Fires on scroll with the current scrollTop value. |
onRangeChange | (start, end) => void | — | Fires when the visible index range changes. |
stickyHeader | ReactNode | — | Header rendered inside the scroll container with position: sticky. |
stickyHeaderHeight | number | 0 | Height of stickyHeader. Subtracted from visible-area calculations so items don't render under it. |
className | string | — | Extra classes on the container. |