build69
build69  /  Components  /  Data Display  /  VirtualList

VirtualList

v1.0.2NewUpdated 27 Jul 2026

Windowed list built on @tanstack/react-virtual that renders only the visible slice of arbitrarily long datasets while keeping list/listitem semantics, roving-tabindex keyboard navigation, and aria-setsize / aria-posinset

Preview

Live · 10 stories
DefaultOpen ↗
Why this is recommendedvirtualizedwindowinginfinite-scrollperformance+4

The default, out-of-the-box VirtualList.

Recommended for+14
  • Invoice / order registers with thousands of rows
  • Product or SKU pickers over large catalogs
  • Chat and activity logs
  • Infinite-loading feeds
Tags+6
virtualizedwindowinginfinite-scrollperformancelarge-dataliststicky-headersdefault
Features+4
  • Renders only visible rows — 10k+ items stay fast
  • Fixed or per-item estimateSize, plus dynamic measurement mode
  • Grouped mode with sticky group headers
  • Horizontal orientation
  • Imperative scrollToIndex / scrollToOffset via ref (reduced-motion aware smooth scroll)
  • onEndReached + endReachedThreshold for infinite loading with loadingFooter slot
  • Empty-state slot
  • Error-state slot (role="alert") with Retry — outranks empty / loading / content
  • Roving-tabindex keyboard navigation (Arrows / Home / End scroll the focused row into view)
  • Controlled or uncontrolled activeIndex
  • aria-setsize / aria-posinset announce the true list length
Not recommended for−2
  • Short lists under ~100 rows (use List)
  • Tabular data with sortable columns (use DataTable)

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "VirtualList" })

Usage

import { VirtualList } from '@/components/data-display/virtual-list';

<VirtualList
  aria-label="Invoices"
  items={invoices}
  height={360}
  estimateSize={52}
  getItemKey={(inv) => inv.id}
  onEndReached={loadMore}
  isLoading={loading}
  loadingFooter={<span>Loading…</span>}
  renderItem={(inv) => <InvoiceRow invoice={inv} />}
/>

Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
items

Flat items (ignored when groups is set)

T[]
groups

Grouped items with sticky headers

VirtualListGroup<T>[]
renderItem

Row renderer; index is the global item index

(item: T, index: number) => ReactNodeyes
estimateSize

Estimated row size in px (default 44)

number | ((index: number) => number)
dynamic

Measure rows after render for variable heights

boolean
overscan

Extra rows rendered outside the viewport (default 4)

number
height

Scroll viewport height (default 320)

number | string
horizontal

Scroll along the x-axis

boolean
onEndReached

Infinite-load callback fired near the end

() => void
activeIndex

Controlled keyboard-active row (uncontrolled via defaultActiveIndex)

number
error

Error state: replaces the body with a role="alert" region (outranks empty/loading/content); true for the default localized message, or a node for a custom message + Retry

React.ReactNode
ref

Imperative scrollToIndex / scrollToOffset handle

Ref<VirtualListHandle>

Related