Virtual scrolling renders only visible items, enabling smooth scrolling through 100K+ rows.
Step 1: Install TanStack Virtual
pnpm add @tanstack/react-virtual
Step 2: Basic Virtual List
"use client";
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
const items = Array.from({ length: 50000 }, (_, i) => ({
id: i,
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
}));
export function VirtualList() {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated row height in px
overscan: 5, // Extra items rendered outside viewport
});
return (
<div ref={parentRef} className="h-[500px] overflow-auto rounded-xl border dark:border-gray-700">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = items[virtualItem.index];
return (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
className="flex items-center justify-between border-b px-4 dark:border-gray-800"
>
<span className="text-sm">{item.name}</span>
<span className="text-sm text-gray-500">{item.value}</span>
</div>
);
})}
</div>
</div>
);
}
Step 3: Virtual Table
"use client";
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
interface Column<T> {
key: keyof T;
label: string;
width: number;
}
interface VirtualTableProps<T> {
data: T[];
columns: Column<T>[];
rowHeight?: number;
}
export function VirtualTable<T extends { id: string | number }>({
data,
columns,
rowHeight = 40,
}: VirtualTableProps<T>) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => rowHeight,
overscan: 10,
});
const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);
return (
<div className="rounded-xl border dark:border-gray-700">
{/* Header */}
<div
className="flex border-b bg-gray-50 dark:border-gray-700 dark:bg-gray-800"
style={{ width: totalWidth }}
>
{columns.map((col) => (
<div
key={String(col.key)}
className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500"
style={{ width: col.width }}
>
{col.label}
</div>
))}
</div>
{/* Body */}
<div
ref={parentRef}
className="overflow-auto"
style={{ height: "500px" }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: totalWidth,
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = data[virtualRow.index];
return (
<div
key={virtualRow.key}
className="flex border-b hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/50"
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{columns.map((col) => (
<div
key={String(col.key)}
className="flex items-center px-3 text-sm"
style={{ width: col.width }}
>
{String(row[col.key])}
</div>
))}
</div>
);
})}
</div>
</div>
{/* Footer */}
<div className="border-t px-3 py-2 text-xs text-gray-500 dark:border-gray-700">
{data.length.toLocaleString()} rows
</div>
</div>
);
}
Step 4: Variable Height Items
"use client";
import { useRef, useCallback } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
const messages = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: "Lorem ipsum ".repeat(Math.floor(Math.random() * 10) + 1).trim(),
author: `User ${Math.floor(Math.random() * 100)}`,
}));
export function VariableHeightVirtualList() {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // Rough estimate
overscan: 5,
});
const measureElement = useCallback(
(el: HTMLDivElement | null) => {
if (!el) return;
const index = Number(el.dataset.index);
virtualizer.measureElement(el);
},
[virtualizer]
);
return (
<div ref={parentRef} className="h-[500px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const message = messages[virtualItem.index];
return (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualItem.start}px)`,
}}
className="border-b p-3 dark:border-gray-800"
>
<p className="text-xs font-medium text-blue-600">
{message.author}
</p>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{message.text}
</p>
</div>
);
})}
</div>
</div>
);
}
Step 5: Virtual Grid
"use client";
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
export function VirtualGrid({ items }: { items: { id: number; src: string }[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const columns = 4;
const rowCount = Math.ceil(items.length / columns);
const virtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
overscan: 2,
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * columns;
const rowItems = items.slice(startIndex, startIndex + columns);
return (
<div
key={virtualRow.key}
className="grid grid-cols-4 gap-2 p-1"
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{rowItems.map((item) => (
<div
key={item.id}
className="overflow-hidden rounded-lg border dark:border-gray-700"
>
<img
src={item.src}
alt=""
className="h-full w-full object-cover"
loading="lazy"
/>
</div>
))}
</div>
);
})}
</div>
</div>
);
}
Need High-Performance Data UIs?
We build web applications optimized for large datasets with virtual scrolling, lazy loading, and fast data tables. Contact us to discuss your project.