Data display

Virtual list

Windowed list: renders only the rows on screen.

Client componentSource

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.

50,000 rows

Rendering rows 00 — everything else is a scrollbar offset, not a DOM node.

Customer 0

customer0@example.com

Active

Customer 1

customer1@example.com

Invited

Customer 2

customer2@example.com

Suspended

Customer 3

customer3@example.com

Active

Customer 4

customer4@example.com

Invited
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.

Props for VirtualList
PropTypeDefaultDescription
items requiredreadonly T[]The full dataset. Only the visible slice is ever rendered.
itemHeight requirednumber | ((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) => ReactNodeRenders one row. Must not itself be tall enough to scroll.
overscannumberExtra 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 | numberStable key for a row. Strongly recommended — see the note on the default below.
labelstringAccessible name for the scrollable region.
scrollToIndexnumberScroll this index into view whenever it changes.
onRangeChange(range: { start: number; end: number; }) => voidFired 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