What is Virtual scrolling?

Virtual scrolling: Virtual scrolling renders only the rows of a long list that are visible in the viewport, plus a small buffer, so a list of 100,000 items stays fast.

Virtual scrolling, also called windowing or list virtualization, keeps a long list fast by not rendering most of it. The scroll container gets a spacer as tall as the whole list would be, so the scrollbar is right, and only the rows that fall inside the viewport, plus a few above and below as overscan, exist in the DOM. As the user scrolls, rows are positioned at their offsets and recycled. The browser lays out and paints a few dozen elements instead of tens of thousands, which is the difference between a smooth log viewer and a tab that hangs.

When to use it

  • Lists and tables past a few hundred rows that are all loaded in memory, such as logs, file lists, large option lists and spreadsheets.
  • Chat and feed histories combined with infinite scroll, where the DOM would otherwise grow without bound.
  • Not for short lists. Virtualization adds complexity, breaks browser find-in-page for off-screen rows and complicates printing. Below a few hundred rows, plain rendering with content-visibility: auto on each row is often enough.
  • Not a substitute for [pagination](/glossary/pagination) when data lives on the server. Virtualize what you have; page what you fetch.

How it works

  • The container has a fixed height and overflow: auto. The total height is the row count times the row height for fixed rows, or a running sum of measured heights for variable rows.
  • On scroll, the first visible index is scrollTop / rowHeight rounded down and the last is that plus the viewport height over the row height; widen by the overscan and render only those indexes.
  • Each rendered row is placed with position: absolute and a top, or a transform: translateY(), at its offset.
  • Variable heights are measured with a ResizeObserver after a row renders and the offsets are updated; an estimate is used until then, which is why fast scrolling can show a small jump.
  • Keys must be stable per item, not per index, so React reuses the right nodes.

Accessibility

Screen readers only see the rendered rows, so a 10,000 row list looks like 30 rows. Fix that with aria-rowcount on a grid, or aria-setsize and aria-posinset on list items, so the position is announced correctly. Keyboard navigation must move an active row and scroll it into view, since Tab cannot reach rows that do not exist yet. Provide a way to jump to a row by number or search, and announce the active row through a polite live region when focus stays on the container.

How to build one in React

MiniDev's VirtualList handles fixed or measured row heights, overscan, keyboard row navigation and scroll to index:

tsx
"use client"
import { VirtualList } from "@/components/ui/virtual-list"

type Row = { id: string; number: string; customer: string }

export function InvoiceList({ rows }: { rows: Row[] }) {
  return (
    <VirtualList
      items={rows}
      rowHeight={44}
      height={480}
      aria-label="Invoices"
      renderRow={(r) => (
        <div className="flex items-center justify-between px-3 text-sm">
          <span>{r.number}</span>
          <span className="text-fg-muted">{r.customer}</span>
        </div>
      )}
    />
  )
}

VirtualizedList is a smaller fixed-height version that takes a count and a renderItem, and is easy to read if you want to see the arithmetic. For tables, TanStack Virtual pairs well with the DataTable. All of them are in the data tables category.

Virtual ListuiA React virtual list that renders only the rows in view, with fixed or measured row heights, overscan, arrow key and Page Up/Down navigation and a scrollToIndex handle.npx shadcn@latest add @minidev/virtual-listVirtualized ListuiVirtualized list that renders only the visible rows plus a small buffer, using a fixed item height and absolute positioning. Keeps thousands of React rows fast.npx shadcn@latest add @minidev/virtualized-list