Virtual List

A long list that draws only the rows in view, so thousands of rows scroll smoothly.
Beta
Web
iOS
Android
@advui/data
Source

Ten thousand rows

INV-00001Shwe Taung
$100.00
INV-00002Golden Lotus
$179.19
INV-00003Irrawaddy Trading
$258.38
INV-00004Mandalay Print
$337.57
INV-00005Yoma Foods
$416.76
INV-00006Shwe Taung
$495.95
INV-00007Golden Lotus
$575.14
INV-00008Irrawaddy Trading
$654.33
INV-00009Mandalay Print
$733.52
INV-00010Yoma Foods
$812.71

Installation

pnpm
pnpm add @advui/data
npm
npm install @advui/data

Usage

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.
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.
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.
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.
PropTypeDefaultDescription
data requiredreadonly 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.
itemHeightnumber | ((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.
estimatedItemHeightnumber48Without `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-labelstring—Names the list for screen readers. Recommended.
overscannumber4Rows drawn above and below the visible ones, so fast scrolling shows no gap.Web only.
initialCountnumber10Rows 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.
onEndReachedThresholdnumber0.5How near the end `onEndReached` fires, in visible heights.
footerReactNode—Shown after the last row, such as a loading row.
emptyReactNode—Shown instead of the list when `data` is empty, such as an Empty State.

Accessibility

  • On web it is a ScrollArea (a named region that takes focus, so the arrow keys scroll it) holding a list.
  • Only the drawn rows are in the page, so each listitem has aria-posinset and aria-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.
Keyboard interactions
KeyAction
TabFocuses the list, then moves through what its rows hold.
Arrow keys / Page Up / Page DownScroll while the list has focus.
Home / EndScroll 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.
Since v0.14.0Last updated Nov 2, 2026Edit this page on GitHub