Virtual List
A long list that draws only the rows in view, so thousands of rows scroll smoothly.Ten thousand rows
Preview
Code
INV-00001Shwe Taung
$100.00INV-00002Golden Lotus
$179.19INV-00003Irrawaddy Trading
$258.38INV-00004Mandalay Print
$337.57INV-00005Yoma Foods
$416.76INV-00006Shwe Taung
$495.95INV-00007Golden Lotus
$575.14INV-00008Irrawaddy Trading
$654.33INV-00009Mandalay Print
$733.52INV-00010Yoma Foods
$812.71Installation
Package
CLI (copy source)
pnpm
pnpm add @advui/datanpm
npm install @advui/dataUsage
import { Text } from '@advui/core'
import { VirtualList } from '@advui/data'
<VirtualList
aria-label="Invoices"
height="80"
data={invoices}
itemHeight={56}
getItemKey={(invoice) => invoice.id}
renderItem={(invoice) => <Text>{invoice.id}</Text>}
/>Examples
Load more at the end
`onEndReached` loads the next page and `footer` shows that it is loading.Preview
Code
Order 4800 shipped
Order 4799 shipped
Order 4798 shipped
Order 4797 shipped
Order 4796 shipped
Order 4795 shipped
Order 4794 shipped
Order 4793 shipped
Order 4792 shipped
Order 4791 shipped
Rows of different heights
`itemHeight` as a function: day headings are shorter than messages.Preview
Code
October 1
ThiriSent the township figures.
Kyaw ZinThe Bago sheet needs one more column.
MayCan we move the review to Thursday?
HtetUploaded the new map.
ThiriSent the township figures.
Kyaw ZinThe Bago sheet needs one more column.
October 2
Kyaw ZinUploaded the new map.
MaySent the township figures.
Rows that size themselves
No `itemHeight`: messages wrap to as many lines as they need.Preview
Code
ThiriSent the township figures.
Kyaw ZinUploaded the new map. It has the camps from the last round, and the roads the cluster asked for are on their own layer so they can be hidden.
MayThe Bago sheet needs one more column before we can merge it with the others, and the dates in it are in two formats.
HtetThanks!
ThiriCan we move the review to Thursday?
Kyaw ZinSent the township figures.
MayUploaded the new map. It has the camps from the last round, and the roads the cluster asked for are on their own layer so they can be hidden.
HtetThe Bago sheet needs one more column before we can merge it with the others, and the dates in it are in two formats.
ThiriThanks!
Kyaw ZinCan we move the review to Thursday?
API reference
VirtualList
Give it a height (`height`, or `flex` in a sized parent). Takes ScrollArea’s size and border props.| Prop | Type | Default | Description |
|---|---|---|---|
data required | readonly T[] | — | The rows, in order. |
renderItem required | (item: T, index: number) => ReactNode | — | Draws one row: in a box of the row’s height with `itemHeight`, or as tall as it draws itself without. |
itemHeight | number | ((index: number) => number) | — | Row height in pixels: one for every row, or one per row. Leave it out for rows that size themselves, such as wrapping text: they are measured as they are drawn. |
estimatedItemHeight | number | 48 | Without `itemHeight`: the height of a row not yet measured, which places the rows below it and sizes the scrollbar until then. |
getItemKey | (item: T, index: number) => string | number | — | A stable key per row, such as its id. Default: the index. |
aria-label | string | — | Names the list for screen readers. Recommended. |
overscan | number | 4 | Rows drawn above and below the visible ones, so fast scrolling shows no gap.Web only. |
initialCount | number | 10 | Rows drawn before the list is measured: on the server and in the first frame. |
onEndReached | () => void | — | Called when the end comes near, once for each length of `data`: load the next page here. |
onEndReachedThreshold | number | 0.5 | How near the end `onEndReached` fires, in visible heights. |
footer | ReactNode | — | Shown after the last row, such as a loading row. |
empty | ReactNode | — | Shown instead of the list when `data` is empty, such as an Empty State. |
Accessibility
- On web it is a ScrollArea (a named
regionthat takes focus, so the arrow keys scroll it) holding alist. - Only the drawn rows are in the page, so each
listitemhasaria-posinsetandaria-setsize: screen readers say “3 of 10,000”. - A row that holds focus stays drawn when it scrolls out of view, so keyboard focus is never lost. Tab moves through the drawn rows, and the list scrolls to each.
- On iOS and Android it is a FlatList named with
accessibilityLabel; VoiceOver and TalkBack scroll it as they move from row to row.
| Key | Action |
|---|---|
| Tab | Focuses the list, then moves through what its rows hold. |
| Arrow keys / Page Up / Page Down | Scroll while the list has focus. |
| Home / End | Scroll to the first or last row. |
Platform notes
Web
Rows are placed at their offsets in a box as tall as the whole list, so the scrollbar is true to length. Rows that size themselves are measured with a ResizeObserver as they are drawn; when one above the view changes height, the list scrolls by as much, so what is in view stays put.
iOS
A FlatList with
getItemLayout from itemHeight, so it places rows without measuring them; without itemHeight, FlatList measures each row it draws. It keeps its own window of rows; overscan is ignored. In a scrolling screen it still windows its rows, as it has a height, so React Native’s warning about lists nested in scroll views is not raised.Android
Same as iOS, with nested scrolling on, so the list scrolls inside a scrolling screen.