Virtual
Long lists and galleries rendered by what is in view, on TanStack Virtual: a list in its own region or the page's, a grid that fits the width, measured items, the end announced for infinite loading.
Web only — React, from @krizaka/ui/virtual. Beta: its API may still change in a minor version.
When to use
- A list of hundreds to millions of items loaded at once or page by page: messages, comments, logs, notifications.
- A gallery that grows as the person scrolls: videos, photos, products (
VirtualGrid). - A feed that scrolls the page itself rather than a box (
scroll="window").
When not to use
- For records compared on columns: the data table has its own windowing.Use instead: Data table
- For a short list (under a hundred items): render it whole, with no scrolling region.Use instead: Card grid
- In a React Native app: FlashList, through the native list.Use instead: List
Installation
Install
npm install @krizaka/ui @krizaka/tailwind @krizaka/tokens tailwindcssStyles
@import "tailwindcss";
@import "@krizaka/tailwind";
@import "@krizaka/ui/tailwind.css";Import
import { VirtualList, VirtualGrid, useWindowVirtualizer, useVirtualizer } from "@krizaka/ui/virtual";Examples
50,000 messages
A VirtualList of items of different heights, each measured.
Noah
On my way.
Elena
Can you send the cut from yesterday? The second take, with the sunset over the river and the drone shot at the end.
Noah
Done!
Elena
Thanks — looks great, posting it tonight at eight. I'll tag you.
Noah
On my way.
Elena
Can you send the cut from yesterday? The second take, with the sunset over the river and the drone shot at the end.
Noah
Done!
Elena
Thanks — looks great, posting it tonight at eight. I'll tag you.
Noah
On my way.
Elena
Can you send the cut from yesterday? The second take, with the sunset over the river and the drone shot at the end.
Noah
Done!
Elena
Thanks — looks great, posting it tonight at eight. I'll tag you.
A gallery
A VirtualGrid of 50,000 clips: as many columns as the width allows.
Clip 1
1 min
Clip 2
2 min
Clip 3
3 min
Clip 4
4 min
Clip 5
5 min
Clip 6
6 min
A feed
scroll="window": the page scrolls, 20 more posts each time the end comes near.
Props
VirtualGrid
A gallery rendered by the rows in view: as many columns as the width allows (or a fixed number), measured rows.
| Prop | Type | Default | Description |
|---|---|---|---|
estimateRowHeightrequired | number | — | The height of a row in px (a guess: rows are measured). |
itemsrequired | readonly T[] | — | The items, all of them: only the rows in view are rendered. |
labelrequired | string | — | The accessible name of the grid of items ("Videos"), passed translated. |
renderItemrequired | (item: T, index: number) => ReactNode | — | Draws one cell (a card, a thumbnail). |
className | string | — | Classes merged on the scrolling region. |
columns | number | — | A fixed number of columns. Without it, as many columns of at least minItemWidth as the width allows. |
empty | ReactNode | — | Shown instead of the grid when there is no item. |
footer | ReactNode | — | Shown after the last row. |
gap | number | 16 | Space between cells, in px. Default 16. |
getKey | ((item: T, index: number) => string | number) | — | A stable key per item. Default: the index. |
height | string | number | 480 | The height of the scrolling region: px or any CSS length. Default 480. |
minItemWidth | number | 220 | The narrowest a cell may be, in px, when columns is not set. Default 220. |
onEndReached | (() => void) | — | Called when the last rows come near (load the next page). |
overscan | number | 3 | Rows rendered beyond the visible ones. Default 3. |
ref | Ref<VirtualHandle> | — | scrollToIndex (of an item) from outside. |
VirtualList
A long list rendered by what is in view: fixed or measured sizes, its own scrolling region or the page's, the end announced for infinite loading, items placed in the whole for assistive technology.
| Prop | Type | Default | Description |
|---|---|---|---|
estimateSizerequired | Size | — | The size of an item in px, or a guess per index; each item is then measured (dynamic sizes). |
itemsrequired | readonly T[] | — | The items, all of them: only those in view (and overscan around) are rendered. |
labelrequired | string | — | The accessible name of the list ("Messages", "Events"), passed translated. |
renderItemrequired | (item: T, index: number) => ReactNode | — | Draws one item. |
className | string | — | Classes merged on the scrolling region (self) or the list (window). |
empty | ReactNode | — | Shown instead of the list when there is no item. |
endReachedThreshold | number | 5 | How many items before the end onEndReached fires. Default 5. |
footer | ReactNode | — | Shown after the last item (a spinner while the next page loads). |
gap | number | 0 | Space between two items, in px. Default 0. |
getKey | ((item: T, index: number) => string | number) | — | A stable key per item (an id): keeps measurements on the right item when the list changes. Default: the index. |
height | string | number | 400 | The height of the scrolling region (self): px or any CSS length. Default 400. |
onEndReached | (() => void) | — | Called when the end comes near (load the next page): once per length of items. |
overscan | number | 6 | Items rendered beyond the visible ones, on each side. Default 6. |
ref | Ref<VirtualHandle> | — | scrollToIndex from outside. |
scroll | "self" | "window" | self | self (default): the list scrolls in its own region of height. window: the page scrolls (a feed). |
Accessibility
- The scrolling region is focusable (
tabIndex=0) and named bylabel. - Items are a
role="list"oflistitems carryingaria-setsizeandaria-posinset: a screen reader says “item 4,812 of 50,000” though only twenty exist in the page. scrollToIndex(throughref) scrolls smoothly, or at once when the person asked for less motion.
Keyboard
| Keys | Action |
|---|---|
| Tab | Reaches the scrolling region (then the links and buttons of the items in view). |
| Arrow keys | Scroll the focused region; Page Up / Page Down and Home / End move by a screen or to the ends. |
Best practices
- Give
getKey(an id): measurements stay with their item when the list changes. - Estimate the size close to the average (
estimateSize): items are measured anyway, a close guess keeps the scrollbar steady. - Load the next page in
onEndReached(auseInfiniteQueryof @krizaka/query) and show a spinner infooter. - Name the list (
label): it names the focusable scrolling region, so keyboard people can scroll it. - Need the raw engine?
useVirtualizeranduseWindowVirtualizercome through this entry — never import@tanstack/*in a product.
Related components
Generated from the code of @krizaka/ui 3.0.0: its meta.ts, its examples and its types.Edit this documentation on GitHub