TanStack Virtual provides headless row virtualization — useVirtualizer({ count, getScrollElement, estimateSize }) creates a virtualizer instance. virtualizer.getVirtualItems() returns the visible range with key, index, start, and size per item. <div style={{ height: virtualizer.getTotalSize() }}> sets the scroll container height. <div style={{ transform: \translateY(${item.start}px)` }}>positions each row.measureElementcallback enables dynamic height measurement viaref. overscan={5}pre-renders buffer rows.useWindowVirtualizervirtualizes against the window scroll. Bidirectional scroll:scrollToIndex(0, { align: “start” })and dynamic prepend withadjustedOffset. Sticky indices: getVirtualIndexes({ stickyIndices: [0] }). useVirtualizeralso handles horizontal virtualization viahorizontal: true. Column-level virtualization: two nested useVirtualizer` instances for table rows and columns. Claude Code generates TanStack Virtual lists, infinite feeds, and virtualized tables.
CLAUDE.md for TanStack Virtual
## TanStack Virtual Stack
- Version: @tanstack/react-virtual >= 3.10
- Setup: const parentRef = useRef<HTMLDivElement>(null); const virt = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, overscan: 8 })
- Render: <div ref={parentRef} className="overflow-auto h-[500px]"><div style={{ height: virt.getTotalSize() }} className="relative">{virt.getVirtualItems().map(item => <div key={item.key} style={{ position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${item.start}px)` }}>)}</div></div>
- Dynamic: <div key={item.key} ref={virt.measureElement} data-index={item.index} style={{ ... }}>
- Scroll to: virt.scrollToIndex(targetIndex, { align: "center", behavior: "smooth" })
Virtualized List Component
// components/virtual/VirtualList.tsx — TanStack Virtual with dynamic measurement
"use client"
import { useRef, useState, useCallback, useMemo } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
type ListItem = {
id: string
title: string
body: string
meta?: string
badge?: string
badgeColor?: string
}
interface VirtualListProps {
items: ListItem[]
onItemClick?: (item: ListItem) => void
height?: number
estimatedItemHeight?: number
}
export function VirtualList({
items,
onItemClick,
height = 600,
estimatedItemHeight = 88,
}: VirtualListProps) {
const parentRef = useRef<HTMLDivElement>(null)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [query, setQuery] = useState("")
const filtered = useMemo(
() => query
? items.filter((i) => i.title.toLowerCase().includes(query.toLowerCase()) || i.body.toLowerCase().includes(query.toLowerCase()))
: items,
[items, query],
)
const virtualizer = useVirtualizer({
count: filtered.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimatedItemHeight,
overscan: 8,
})
const scrollToTop = useCallback(() => {
virtualizer.scrollToIndex(0, { align: "start", behavior: "smooth" })
}, [virtualizer])
const handleSelect = useCallback((item: ListItem) => {
setSelectedId(item.id)
onItemClick?.(item)
}, [onItemClick])
return (
<div className="flex flex-col border rounded-xl overflow-hidden bg-card" style={{ height }}>
{/* Search */}
<div className="flex items-center gap-3 p-3 border-b">
<input
type="search"
value={query}
onChange={(e) => { setQuery(e.target.value); virtualizer.scrollToOffset(0) }}
placeholder={`Search ${items.length.toLocaleString()} items…`}
className="flex-1 px-3 py-2 text-sm rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button onClick={scrollToTop} className="text-xs text-muted-foreground hover:text-foreground px-2">↑ Top</button>
</div>
{/* Count */}
<div className="px-4 py-1.5 border-b bg-muted/30">
<p className="text-xs text-muted-foreground">
{filtered.length.toLocaleString()} {query ? "results" : "items"}
</p>
</div>
{/* Scroll container */}
<div ref={parentRef} className="flex-1 overflow-auto">
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((vItem) => {
const item = filtered[vItem.index]
if (!item) return null
const isSelected = item.id === selectedId
return (
<div
key={vItem.key}
data-index={vItem.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${vItem.start}px)`,
}}
>
<button
onClick={() => handleSelect(item)}
className={`w-full text-left px-4 py-3 border-b transition-colors ${
isSelected ? "bg-primary/10 border-l-2 border-l-primary" : "hover:bg-muted/50"
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{item.title}</p>
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
{item.body}
</p>
{item.meta && (
<p className="text-xs text-muted-foreground/70 mt-1">{item.meta}</p>
)}
</div>
{item.badge && (
<span
className="flex-shrink-0 text-xs px-2 py-0.5 rounded-full font-medium mt-0.5"
style={{
backgroundColor: `${item.badgeColor ?? "#6366f1"}20`,
color: item.badgeColor ?? "#6366f1",
}}
>
{item.badge}
</span>
)}
</div>
</button>
</div>
)
})}
</div>
</div>
</div>
)
}
Virtualized Data Table
// components/virtual/VirtualTable.tsx — TanStack Virtual + Table with column virtualization
"use client"
import { useRef, useState } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
type Column<T> = {
key: keyof T
header: string
width: number
render?: (value: T[keyof T], row: T) => React.ReactNode
}
interface VirtualTableProps<T extends { id: string }> {
rows: T[]
columns: Column<T>[]
height?: number
rowHeight?: number
}
export function VirtualTable<T extends { id: string }>({
rows,
columns,
height = 500,
rowHeight = 48,
}: VirtualTableProps<T>) {
const scrollRef = useRef<HTMLDivElement>(null)
const [sortKey, setSortKey] = useState<keyof T | null>(null)
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc")
const sorted = sortKey
? [...rows].sort((a, b) => {
const av = a[sortKey], bv = b[sortKey]
const cmp = av < bv ? -1 : av > bv ? 1 : 0
return sortDir === "asc" ? cmp : -cmp
})
: rows
const rowVirtualizer = useVirtualizer({
count: sorted.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => rowHeight,
overscan: 10,
})
const totalWidth = columns.reduce((sum, c) => sum + c.width, 0)
const toggleSort = (key: keyof T) => {
if (sortKey === key) setSortDir((d) => d === "asc" ? "desc" : "asc")
else { setSortKey(key); setSortDir("asc") }
}
return (
<div className="border rounded-xl overflow-hidden bg-card">
{/* Header */}
<div className="overflow-x-auto border-b bg-muted/50" style={{ minWidth: totalWidth }}>
<div className="flex" style={{ width: totalWidth }}>
{columns.map((col) => (
<button
key={String(col.key)}
onClick={() => toggleSort(col.key)}
className="flex items-center gap-1 px-3 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wide text-left hover:text-foreground transition-colors flex-shrink-0"
style={{ width: col.width }}
>
{col.header}
{sortKey === col.key && (
<span className="text-primary">{sortDir === "asc" ? "↑" : "↓"}</span>
)}
</button>
))}
</div>
</div>
{/* Scroll body */}
<div ref={scrollRef} className="overflow-auto" style={{ height }}>
<div style={{ height: rowVirtualizer.getTotalSize(), width: totalWidth, position: "relative" }}>
{rowVirtualizer.getVirtualItems().map((vRow) => {
const row = sorted[vRow.index]
if (!row) return null
return (
<div
key={vRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: rowHeight,
transform: `translateY(${vRow.start}px)`,
}}
className={`flex items-center border-b ${vRow.index % 2 === 0 ? "" : "bg-muted/20"}`}
>
{columns.map((col) => (
<div
key={String(col.key)}
className="px-3 text-sm truncate flex-shrink-0"
style={{ width: col.width }}
>
{col.render
? col.render(row[col.key], row)
: String(row[col.key] ?? "")}
</div>
))}
</div>
)
})}
</div>
</div>
{/* Footer */}
<div className="px-4 py-2 border-t text-xs text-muted-foreground bg-muted/20">
{rows.length.toLocaleString()} rows · {columns.length} columns
</div>
</div>
)
}
For the react-window alternative when a smaller package size, a simpler API with pre-built FixedSizeList/VariableSizeList/FixedSizeGrid components, and an established ecosystem with react-window-infinite-loader is preferred — react-window is more opinionated but easier to drop in while TanStack Virtual is headless with no default DOM structure, ideal for custom grid layouts, see the react-window guide. For TanStack Table when combining row virtualization with column features like sorting, filtering, column resizing, and row grouping — @tanstack/react-table with @tanstack/react-virtual is the canonical pairing for high-performance data grids, see the TanStack Table guide. The Claude Skills 360 bundle includes TanStack Virtual skill sets covering dynamic heights, window virtualization, and table grids. Start with the free tier to try headless virtualization generation.