Data display
Virtual list
Windowed list: renders only the rows on screen.
Import
import { VirtualList } from '@the_viveksingh/vivek-ui'Fifty thousand rows
Only the visible rows exist in the DOM. onRangeChange reports the window, which is also the hook for loading more data as the user scrolls.
Rendering rows 0–0 — everything else is a scrollbar offset, not a DOM node.
Customer 0
customer0@example.com
Customer 1
customer1@example.com
Customer 2
customer2@example.com
Customer 3
customer3@example.com
Customer 4
customer4@example.com
const rows = useMemo(
() => Array.from({ length: 50_000 }, (_, i) => ({ id: i, name: `Customer ${i}` })),
[],
)
<VirtualList
items={rows}
itemHeight={56}
getKey={(row) => row.id}
label="All customers"
onRangeChange={({ start, end }) => console.log(start, end)}
style={{ height: '20rem' }}
>
{(row) => <div>{row.name}</div>}
</VirtualList>Variable row heights
Pass a function instead of a number and it becomes an estimate. Rows are measured as they render and the estimate is replaced, so nothing has to be computed up front.
Customer 0
customer0@example.com · this row is taller, and was measured rather than assumed
Customer 1
Customer 2
Customer 3
customer3@example.com · this row is taller, and was measured rather than assumed
Customer 4
<VirtualList
items={rows}
// An estimate, corrected by measurement once the row renders.
itemHeight={(index) => (index % 3 === 0 ? 88 : 52)}
getKey={(row) => row.id}
label="Customers"
style={{ height: '20rem' }}
>
{(row, index) => (
<div>
<Text weight="medium">{row.name}</Text>
{index % 3 === 0 ? <Text size="sm" tone="muted">{row.email}</Text> : null}
</div>
)}
</VirtualList>Props
Generated from the package's own type declarations, so this table cannot drift from the code.
| Prop | Type | Default | Description |
|---|---|---|---|
items required | readonly T[] | — | The full dataset. Only the visible slice is ever rendered. |
itemHeight required | number | ((index: number, item: T) => number) | — | Row height in pixels. A number means every row is that tall, which is the fast path: the scroll position maps to an index with one division and nothing needs measuring. A function is the estimate for a row whose real height is not known yet. Rows are measured as they render and the estimate is replaced, so a wrong estimate costs accuracy in the scrollbar early on, not correctness. |
children required | (item: T, index: number) => ReactNode | — | Renders one row. Must not itself be tall enough to scroll. |
overscan | number | — | Extra rows rendered above and below the viewport. Default `4`. Zero is tempting and wrong: the browser paints scrolled-in rows a frame late, which reads as a flicker of blank space at speed. |
getKey | (item: T, index: number) => string | number | — | Stable key for a row. Strongly recommended — see the note on the default below. |
label | string | — | Accessible name for the scrollable region. |
scrollToIndex | number | — | Scroll this index into view whenever it changes. |
onRangeChange | (range: { start: number; end: number; }) => void | — | Fired with the visible range whenever it changes. Useful for infinite loading. |
Every remaining prop is spread onto the root element, so all standard HTML and ARIA attributes work. className and style are merged with the library's own, never replaced, and the ref forwards to the root DOM node.
Rendering
VirtualList declares 'use client' because it needs state, effects or event handlers. Importing it into a Server Component creates a client boundary at this component — everything above it stays on the server.