tanstack-virtual
4
总安装量
4
周安装量
#52356
全站排名
安装命令
npx skills add https://github.com/fellipeutaka/leon --skill tanstack-virtual
Agent 安装分布
opencode
4
gemini-cli
4
antigravity
4
claude-code
4
github-copilot
4
codex
4
Skill 文档
TanStack Virtual (React)
Installation
npm install @tanstack/react-virtual
Core Concept
Three required options â everything else is optional:
count â total item count
getScrollElement â () => scrollableElement
estimateSize â (index) => number (px)
You must:
- Create a full-height container using
getTotalSize() - Absolutely position each virtual item using
virtualItem.start
Fixed Size (Vertical)
import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
function List({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
{items[item.index]}
</div>
))}
</div>
</div>
)
}
Dynamic Size (measured at runtime)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // err large for better scroll-to-index accuracy
})
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
data-index={item.index} // required for measureElement
ref={virtualizer.measureElement} // measures on mount/resize
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${item.start}px)`,
// Do NOT set height â virtualizer controls it
}}
>
{items[item.index]}
</div>
))}
Window Virtualizer
import { useRef, useEffect } from 'react'
import { useWindowVirtualizer } from '@tanstack/react-virtual'
function WindowList({ items }: { items: string[] }) {
const listRef = useRef<HTMLDivElement>(null)
const virtualizer = useWindowVirtualizer({
count: items.length,
estimateSize: () => 35,
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',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
}}
>
{items[item.index]}
</div>
))}
</div>
</div>
)
}
scrollMargin = distance from page top to list top. Subtract it from item.start in the transform.
Imperative Scroll
virtualizer.scrollToIndex(50, { align: 'start' }) // 'start' | 'center' | 'end' | 'auto'
virtualizer.scrollToOffset(1200, { behavior: 'smooth' })
Critical Rules
Always:
overflow: autoon scroll container- Explicit
height/widthon scroll container position: relativeon inner container sized togetTotalSize()position: absoluteon each virtual itemtranslateY/translateXfor positioning (nottop/left)data-indexon items when usingmeasureElementitem.keyas React key (notitem.index)scrollMarginfor window virtualizers when content precedes the list
Never:
- Set
heighton dynamically measured items - Omit
overflow: autoon scroll container - Use
smoothscroll behavior with dynamic-size items (documented limitation)
Key Options
| Option | Default | Purpose |
|---|---|---|
overscan |
1 | Extra items rendered outside visible area |
gap |
0 | Spacing between items (px) â use instead of CSS margin |
paddingStart |
0 | Padding before first item (px) |
paddingEnd |
0 | Padding after last item (px) |
horizontal |
false | Horizontal orientation |
lanes |
1 | Columns (vertical) or rows (horizontal) for masonry |
getItemKey |
index | Stable item keys â always set when items have IDs |
enabled |
true | Set false to disable observers and reset state |
isRtl |
false | RTL horizontal scroll |