tanstack-virtual
Skill3 starsChanged 2 months agoApache-2.0View on GitHub
Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular.
- Installs packages
1 other project uses a version of this.
Adopted by
Most recently taken up by
- udecode/plateword for word, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 1
- udecode/plate★ 17k100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository28 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .agents/skills/vercel-composition-patterns/AGENTS.mdAGENTS.md
- .agents/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- argos-data-importSkill
- argos-releaseSkill
- argos-sddSkill
- awwwards-animationsSkill
- baseline-uiSkill
- bun-file-ioSkill
- design-taste-frontendSkill
- fork-syncSkill
- pi-updateSkill
- react-doctorSkill
- shadcnSkill
- tanstack-formSkill
- tanstack-pacerSkill
- tanstack-querySkill
- tanstack-rangerSkill
- tanstack-routerSkill
- tanstack-storeSkill
- tanstack-tableSkill
- turborepoSkill
- vercel-composition-patternsSkill
- vercel-react-best-practicesSkill
- vgpuSkill
- viteSkill
- web-design-guidelinesSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 131
Use measureElement ref for items with unknown heights:
- Very specific: line 351
Add
data-indexattribute: when using measureElement for dynamic sizing - Very specific: line 354
Use
gapoption: instead of margins (margins interfere with measurement) - Very specific: line 355
Use
paddingStart/End: instead of CSS padding on the container - Very specific: line 365
Not providing position: relative on the inner container
- Very specific: line 369
Not memoizing the estimateSize function (causes re-renders)
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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-virtualCore 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: 1Smooth 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()overtopfor GPU-composited positioning - Add
data-indexattribute when usingmeasureElementfor dynamic sizing - Don't set fixed height on dynamically measured items
- Use
getItemKeyfor stable keys when items can reorder - Use
gapoption instead of margins (margins interfere with measurement) - Use
paddingStart/Endinstead of CSS padding on the container - Use
enabled: falseto pause when the list is hidden - Memoize callbacks (
estimateSize,getItemKey,rangeExtractor) - Use
will-change: transformCSS on items for GPU acceleration
Common Pitfalls
- Setting fixed height on dynamically measured items
- Using CSS margins instead of the
gapoption - Forgetting
data-indexwithmeasureElement - Not providing
position: relativeon the inner container - Underestimating
estimateSize(causes scroll jumps) - Setting
overscantoo low for fast scrolling (blank items) - Forgetting to subtract
scrollMarginfromtranslateYin window scrolling - Not memoizing the
estimateSizefunction (causes re-renders)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

