# VirtualListFlow

Same external API as [`VirtualList`](./virtual-list), 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.

## 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. |
