tanstack-virtual

TanStack Virtual provides virtualization logic for rendering only visible items in large lists, grids, and tables. It calculates which items are in the viewport and positions them with absolute positioning, keeping DOM node count minimal regardless of dataset size.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-virtual" with this command: npx skills add tanstack-skills/tanstack-skills/tanstack-skills-tanstack-skills-tanstack-virtual

Overview

TanStack Virtual provides virtualization logic for rendering only visible items in large lists, grids, and tables. It calculates which items are in the viewport and positions them with absolute positioning, keeping DOM node count minimal regardless of dataset size.

Package: @tanstack/react-virtual

Core: @tanstack/virtual-core (framework-agnostic)

Installation

npm install @tanstack/react-virtual

Core Pattern

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualList() { const parentRef = useRef<HTMLDivElement>(null)

const virtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, // estimated row height in px overscan: 5, })

return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: ${virtualizer.getTotalSize()}px, width: '100%', position: 'relative', }} > {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: ${virtualItem.size}px, transform: translateY(${virtualItem.start}px), }} > Row {virtualItem.index} </div> ))} </div> </div> ) }

Virtualizer Options

Required

Option Type Description

count

number

Total number of items

getScrollElement

() => Element | null

Returns scroll container

estimateSize

(index) => number

Estimated item size (overestimate recommended)

Optional

Option Type Default Description

overscan

number

1

Extra items rendered beyond viewport

horizontal

boolean

false

Horizontal virtualization

gap

number

0

Gap between items (px)

lanes

number

1

Number of lanes (masonry/grid)

paddingStart

number

0

Padding before first item

paddingEnd

number

0

Padding after last item

scrollPaddingStart

number

0

Offset for scrollTo positioning

scrollPaddingEnd

number

0

Offset for scrollTo positioning

initialOffset

number

0

Starting scroll position

initialRect

Rect

Initial dimensions (SSR)

enabled

boolean

true

Enable/disable

getItemKey

(index) => Key

(i) => i

Stable key for items

rangeExtractor

(range) => number[]

default Custom visible indices

scrollToFn

(offset, options, instance) => void

default Custom scroll behavior

measureElement

(el, entry, instance) => number

default Custom measurement

onChange

(instance, sync) => void

State change callback

isScrollingResetDelay

number

150

Delay before scroll complete

Virtualizer API

// Get visible items virtualizer.getVirtualItems(): VirtualItem[]

// Get total scrollable size virtualizer.getTotalSize(): number

// Scroll to specific index virtualizer.scrollToIndex(index, { align: 'start' | 'center' | 'end' | 'auto', behavior: 'auto' | 'smooth' })

// Scroll to offset virtualizer.scrollToOffset(offset, options)

// Force recalculation virtualizer.measure()

VirtualItem Properties

interface VirtualItem { key: Key // Unique key index: number // Index in source data start: number // Pixel offset (use for transform) end: number // End pixel offset size: number // Item dimension lane: number // Lane index (multi-column) }

Dynamic/Variable Heights

Use measureElement ref for items with unknown heights:

const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // overestimate })

{virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} data-index={virtualItem.index} // REQUIRED for measurement ref={virtualizer.measureElement} // Attach for dynamic measurement style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: translateY(${virtualItem.start}px), // Do NOT set fixed height - let content determine it }}

{items[virtualItem.index].content}

</div> ))}

Horizontal Virtualization

const virtualizer = useVirtualizer({ count: columns.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, horizontal: true, })

// Use width for container, translateX for positioning <div style={{ width: ${virtualizer.getTotalSize()}px, position: 'relative' }}> {virtualizer.getVirtualItems().map((item) => ( <div style={{ position: 'absolute', height: '100%', width: ${item.size}px, transform: translateX(${item.start}px), }}> Column {item.index} </div> ))} </div>

Grid Virtualization (Two Virtualizers)

function VirtualGrid() { const parentRef = useRef<HTMLDivElement>(null)

const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, overscan: 5, })

const columnVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 100, horizontal: true, overscan: 5, })

return ( <div ref={parentRef} style={{ height: '500px', width: '500px', overflow: 'auto' }}> <div style={{ height: ${rowVirtualizer.getTotalSize()}px, width: ${columnVirtualizer.getTotalSize()}px, position: 'relative', }}> {rowVirtualizer.getVirtualItems().map((virtualRow) => ( <Fragment key={virtualRow.key}> {columnVirtualizer.getVirtualItems().map((virtualColumn) => ( <div key={virtualColumn.key} style={{ position: 'absolute', width: ${virtualColumn.size}px, height: ${virtualRow.size}px, transform: translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px), }} > Cell {virtualRow.index},{virtualColumn.index} </div> ))} </Fragment> ))} </div> </div> ) }

Window Scrolling

import { useWindowVirtualizer } from '@tanstack/react-virtual'

function WindowList() { const listRef = useRef<HTMLDivElement>(null)

const virtualizer = useWindowVirtualizer({ count: 10000, estimateSize: () => 45, overscan: 5, scrollMargin: listRef.current?.offsetTop ?? 0, })

return ( <div ref={listRef}> <div style={{ height: ${virtualizer.getTotalSize()}px, position: 'relative', }}> {virtualizer.getVirtualItems().map((item) => ( <div key={item.key} style={{ position: 'absolute', height: ${item.size}px, transform: translateY(${item.start - virtualizer.options.scrollMargin}px), }} > Row {item.index} </div> ))} </div> </div> ) }

Infinite Scrolling

import { useVirtualizer } from '@tanstack/react-virtual' import { useInfiniteQuery } from '@tanstack/react-query'

function InfiniteList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['items'], queryFn: ({ pageParam = 0 }) => fetchItems(pageParam), getNextPageParam: (lastPage) => lastPage.nextCursor, })

const allItems = data?.pages.flatMap((page) => page.items) ?? []

const virtualizer = useVirtualizer({ count: hasNextPage ? allItems.length + 1 : allItems.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, overscan: 5, })

useEffect(() => { const items = virtualizer.getVirtualItems() const lastItem = items[items.length - 1] if (lastItem && lastItem.index >= allItems.length - 1 && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, [virtualizer.getVirtualItems(), hasNextPage, isFetchingNextPage, allItems.length])

// Render virtual items, show loader row for last item if loading }

Sticky Items

import { defaultRangeExtractor, Range } from '@tanstack/react-virtual'

const stickyIndexes = [0, 10, 20, 30] // Header indices

const virtualizer = useVirtualizer({ count: 1000, getScrollElement: () => parentRef.current, estimateSize: () => 50, rangeExtractor: useCallback((range: Range) => { const next = new Set([...stickyIndexes, ...defaultRangeExtractor(range)]) return [...next].sort((a, b) => a - b) }, [stickyIndexes]), })

// Render sticky items with position: sticky; top: 0; zIndex: 1

Smooth Scrolling

const virtualizer = useVirtualizer({ scrollToFn: (offset, { behavior }, instance) => { if (behavior === 'smooth') { // Custom easing animation instance.scrollElement?.scrollTo({ top: offset, behavior: 'smooth' }) } else { instance.scrollElement?.scrollTo({ top: offset }) } }, })

// Usage virtualizer.scrollToIndex(500, { align: 'center', behavior: 'smooth' })

Best Practices

  • Overestimate estimateSize

  • prevents scroll jumps (items shrinking causes issues)

  • Increase overscan (3-5) to reduce blank flashing during fast scrolling

  • Use transform: translateY() over top for GPU-composited positioning

  • Add data-index attribute when using measureElement for dynamic sizing

  • Don't set fixed height on dynamically measured items

  • Use getItemKey for stable keys when items can reorder

  • Use gap option instead of margins (margins interfere with measurement)

  • Use paddingStart/End instead of CSS padding on the container

  • Use enabled: false to pause when the list is hidden

  • Memoize callbacks (estimateSize , getItemKey , rangeExtractor )

  • Use will-change: transform CSS on items for GPU acceleration

Common Pitfalls

  • Setting fixed height on dynamically measured items

  • Using CSS margins instead of the gap option

  • Forgetting data-index with measureElement

  • Not providing position: relative on the inner container

  • Underestimating estimateSize (causes scroll jumps)

  • Setting overscan too low for fast scrolling (blank items)

  • Forgetting to subtract scrollMargin from translateY in window scrolling

  • Not memoizing the estimateSize function (causes re-renders)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-table

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-router

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-form

No summary provided by upstream source.

Repository SourceNeeds Review