VirtualList
VirtualList renders only the items near the viewport, so a list of ten thousand rows costs about as much as a screenful. Items can be any height and can change height at any time (an image loads, a message expands): each item is measured as it renders and its height is remembered by key. Whatever you are looking at stays still while items above it load, grow or shrink.
With anchor="end" it is a chat timeline: it starts at the bottom, follows new items while you are at the bottom, keeps your place while older items load above, and can jump to any loaded item.
Demos
Basic
Give the list a height and an estimated item height (itemSize); real heights are measured. scrollToKey lands an item exactly where asked even though most of the heights on the way were estimates.
Chat
anchor="end" rests at the bottom and follows new messages while you are there. Scroll up and it stops following: new messages no longer move the view. Older messages load above (onStartReached) without moving what you are reading, even when they turn out taller than estimated; images here have no known size and grow when they load. Send calls followEnd() so your own message always scrolls into view. Jump to #321 loads a window around a message that is not loaded and jumps to it; the list then knows newer messages exist (hasNewer), so it does not follow, and loads them as you scroll down (onEndReached).
Playground
A test bench for scroll behavior, like vueuc’s virtual list demo: 100,000 rows of mixed heights. Jump by index or key with each alignment, smooth or not; prepend and append; insert after a row (it grows in over 0.3s) or remove one (it collapses); grow a row; change the heights of the rows in view or of every row; hide and show the list; switch between start and end anchors. The numbers below update as you scroll. Whatever is at the top of the view should never move unless you scroll.
How it works
- Heights are cached by key, so they survive prepending, trimming and items scrolling out and back. Offsets come from a Fenwick tree: changing one height and finding the item at an offset are both O(log n).
- One owner of the scroll offset at a time. The list keeps a position: an item and its distance from the top of the viewport, the bottom (following), or a jump target. While you read (anchored), the browser’s own scroll anchoring stays on: when something above the view changes size, the browser moves the offset during layout, and the list, checking right after it measures (before paint), sees that the offset already moved and accepts it instead of correcting again. It corrects only what the browser did not (at offset 0 browsers don’t anchor; when the browser doesn’t support it). While following the bottom or jumping, the list switches the browser’s anchoring off on the scroll container and sets the offset itself. Spacers are never anchor candidates. The playground shows both counts.
- Following changes only when the user scrolls (wheel, touch, keys, the scrollbar). Layout changes, new items and the on-screen keyboard never start or stop it. When the viewport shrinks (keyboard, address bar) a following list stays at the bottom; otherwise it keeps the item in view.
- Jumps re-aim every frame until the target stays put for two frames (at most a second), so items measured on the way don’t make it land short. A long smooth scroll starts near the target. Any user scroll cancels a jump.
- Only rendered items are in the page, so the browser’s find-in-page only finds those.
Props
| Name | Type | Default | Description | Version |
|---|---|---|---|---|
items | T[] | (required) | The items, in display order. | Unreleased |
renderItem | (item: T, index: number) => ReactNode | (required) | Renders one item. Its height is measured; keep this function stable (useCallback). | Unreleased |
keyField | string | key | The item field with a stable, unique key. Heights are remembered by key. | Unreleased |
itemSize | number | (item: T, index: number) => number | 48 | Estimated height of an item not yet measured, in px. | Unreleased |
overscan | number | 400 | Rendered distance beyond each edge of the viewport, in px. | Unreleased |
anchor | start | end | start | end rests at the bottom and follows new items there. | Unreleased |
hasNewer | boolean | false | The last item is not the newest: the list does not follow, and the bottom is not the live end. | Unreleased |
initialKey | string | number | undefined | Item to show first instead of the top (start) or bottom (end). | Unreleased |
initialAlign | start | center | end | nearest | center | Alignment of initialKey. | Unreleased |
endThreshold | number | 100 | Distance from the bottom that still counts as at the end, in px. | Unreleased |
reachThreshold | number | 200 | Distance from an edge that fires onStartReached / onEndReached, in px. | Unreleased |
onStartReached | () => void | undefined | Near the top: load older items. Fires once per first item, so prepending re-arms it. | Unreleased |
onEndReached | () => void | undefined | Near the bottom: load newer items. Fires once per last item. | Unreleased |
onAtEndChange | (atEnd: boolean) => void | undefined | Whether the list is at the bottom (show a back-to-latest button). | Unreleased |
onVisibleRangeChange | (range: VirtualListVisibleRange) => void | undefined | The items in view changed (without overscan). | Unreleased |
deferCorrectionsWhileTouching | boolean | false | Hold height corrections while a finger is down or momentum scrolling runs, and apply them when scrolling stops (iOS cancels momentum when the offset is written). | Unreleased |
header | ReactNode | undefined | Content above the first item, inside the scroll container. | Unreleased |
footer | ReactNode | undefined | Content below the last item, inside the scroll container. | Unreleased |
itemClassName | string | undefined | Class of each item wrapper. | Unreleased |
size | small | medium | medium | Scrollbar thickness, as on ScrollArea. | Unreleased |
trigger | hover | none | hover | When the scrollbar shows, as on ScrollArea. | Unreleased |
Each item is wrapped in a div with data-key and data-index. The list also takes className, style and aria-label.
VirtualList Methods
| Name | Type | Description | Version |
|---|---|---|---|
scrollToKey | (key, options?: { align?, smooth? }) => boolean | Scrolls an item into view (align defaults to start). Returns false when the key is not loaded. | Unreleased |
scrollToIndex | (index, options?) => boolean | Same, by index. | Unreleased |
scrollToStart | () => void | Goes to the first item. | Unreleased |
scrollToEnd | () => void | Goes to the last item; an end list follows again. | Unreleased |
followEnd | () => void | Follows the next items even if the user scrolled away, for example right after sending. | Unreleased |
holdAnchor | () => void | Stops following and keeps the current view, before a layout change you cause (opening a side panel). | Unreleased |
isAtEnd | () => boolean | Within endThreshold of the bottom. | Unreleased |
isFollowing | () => boolean | An end list that stays at the bottom as items arrive or grow. | Unreleased |
getScrollElement | () => HTMLDivElement | null | The scroll container. | Unreleased |
getScrollStats | () => { written, accepted } | Diagnostics: scroll corrections the list wrote, and browser scroll-anchoring adjustments it accepted instead. | Unreleased |