.agents/skills/tanstack-virtual/SKILL.md
Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular. Includes SolidJS examples using @tanstack/solid-virtual.
npx skillsauth add em-jones/staccato-toolkit tanstack-virtualInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Core: @tanstack/virtual-core (framework-agnostic)
# React
npm install @tanstack/react-virtual
# Solid
npm install @tanstack/solid-virtual
| Framework | Package | Hook/Function |
|-----------|---------|----------------|
| React | @tanstack/react-virtual | useVirtualizer, useWindowVirtualizer |
| Solid | @tanstack/solid-virtual | createVirtualizer, createWindowVirtualizer |
| Vue | @tanstack/vue-virtual | useVirtualizer |
| Svelte | @tanstack/svelte-virtual | createVirtualizer |
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList() {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
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>
)
}
import { createVirtualizer } from '@tanstack/solid-virtual'
import { For } from 'solid-js'
function VirtualList() {
let parentRef: HTMLDivElement | undefined
const virtualizer = createVirtualizer({
get count() { return 10000 },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 35,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
<For each={virtualizer.getVirtualItems()}>
{(virtualItem) => (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
Row {virtualItem.index}
</div>
)}
</For>
</div>
</div>
)
}
Key SolidJS differences:
createVirtualizer instead of useVirtualizer<For> instead of .map()get count() { return items.length })ref={parentRef} or let parentRef: HTMLDivElement | undefined| Option | Type | Description |
|--------|------|-------------|
| count | number | Total number of items |
| getScrollElement | () => Element \| null | Returns scroll container |
| estimateSize | (index) => number | Estimated item size (overestimate recommended) |
| 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 |
// 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()
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)
}
Use measureElement ref for items with unknown heights:
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
})
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].content}
</div>
))}
const virtualizer = createVirtualizer({
get count() { return items.length },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 50,
})
<For each={virtualizer.getVirtualItems()}>
{(virtualItem) => (
<div
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].content}
</div>
)}
</For>
const virtualizer = useVirtualizer({
count: columns.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
horizontal: true,
})
<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>
const virtualizer = createVirtualizer({
get count() { return columns.length },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 100,
horizontal: true,
})
<div style={{ width: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
<For each={virtualizer.getVirtualItems()}>
{(item) => (
<div style={{
position: 'absolute',
height: '100%',
width: `${item.size}px`,
transform: `translateX(${item.start}px)`,
}}>
Column {item.index}
</div>
)}
</For>
</div>
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>
)
}
function VirtualGrid() {
let parentRef: HTMLDivElement | undefined
const rowVirtualizer = createVirtualizer({
get count() { return 10000 },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 35,
overscan: 5,
})
const columnVirtualizer = createVirtualizer({
get count() { return 10000 },
getScrollElement: () => parentRef ?? null,
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',
}}>
<For each={rowVirtualizer.getVirtualItems()}>
{(virtualRow) => (
<For each={columnVirtualizer.getVirtualItems()}>
{(virtualColumn) => (
<div
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>
)}
</For>
)}
</For>
</div>
</div>
)
}
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>
)
}
import { createWindowVirtualizer } from '@tanstack/solid-virtual'
import { For } from 'solid-js'
function WindowList() {
let listRef: HTMLDivElement | undefined
const virtualizer = createWindowVirtualizer({
get count() { return 10000 },
estimateSize: () => 45,
overscan: 5,
get scrollMargin() { return listRef?.offsetTop ?? 0 },
})
return (
<div ref={listRef}>
<div style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}>
<For each={virtualizer.getVirtualItems()}>
{(item) => (
<div
style={{
position: 'absolute',
height: `${item.size}px`,
transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
}}
>
Row {item.index}
</div>
)}
</For>
</div>
</div>
)
}
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])
}
import { createVirtualizer } from '@tanstack/solid-virtual'
import { createResource, createEffect, For } from 'solid-js'
async function fetchItems(cursor: number) {
const res = await fetch(`/api/items?cursor=${cursor}`)
return res.json()
}
function InfiniteList() {
let parentRef: HTMLDivElement | undefined
const [items, { refetch }] = createResource(() => fetchItems(0))
const virtualizer = createVirtualizer({
get count() { return (items()?.length ?? 0) + (hasNextPage() ? 1 : 0) },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 50,
overscan: 5,
})
createEffect(() => {
const virtualItems = virtualizer.getVirtualItems()
const lastItem = virtualItems[virtualItems.length - 1]
if (lastItem && lastItem.index >= (items()?.length ?? 0) - 1 && hasNextPage() && !isFetchingNextPage()) {
fetchNextPage()
}
})
}
import { defaultRangeExtractor, Range } from '@tanstack/react-virtual'
const stickyIndexes = [0, 10, 20, 30]
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]),
})
import { defaultRangeExtractor } from '@tanstack/virtual-core'
const stickyIndexes = [0, 10, 20, 30]
const virtualizer = createVirtualizer({
get count() { return 1000 },
getScrollElement: () => parentRef ?? null,
estimateSize: () => 50,
rangeExtractor: (range) => {
const next = new Set([...stickyIndexes, ...defaultRangeExtractor(range)])
return [...next].sort((a, b) => a - b)
},
})
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' })
estimateSize - prevents scroll jumps (items shrinking causes issues)overscan (3-5) to reduce blank flashing during fast scrollingtransform: translateY() over top for GPU-composited positioningdata-index attribute when using measureElement for dynamic sizinggetItemKey for stable keys when items can reordergap option instead of margins (margins interfere with measurement)paddingStart/End instead of CSS padding on the containerenabled: false to pause when the list is hiddenestimateSize, getItemKey, rangeExtractor)will-change: transform CSS on items for GPU accelerationget count() { return items.length }@tanstack/virtual-core for utilities like defaultRangeExtractor<For> component handles keying automatically via the itemgap optiondata-index with measureElementposition: relative on the inner containerestimateSize (causes scroll jumps)overscan too low for fast scrolling (blank items)scrollMargin from translateY in window scrollingestimateSize function (causes re-renders)tools
<!--VITE PLUS START--> # Using Vite+, the Unified Toolchain for the Web This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`. ## Vite+ Workflow `vp` is a global binary that handles the full development lifecycle. Run `vp help` to pr
development
Guide for building performant data tables. Uses tanstack-table for table logic (sorting, filtering, pagination) and tanstack-virtual for rendering large datasets efficiently.
development
Expert guidance for building observable, expressive, and fault-tolerant TypeScript applications using the effect-ts/effect ecosystem. Covers Effect<A, E, R> type, error management, dependency injection via Layers, observability (logging, metrics, tracing), concurrency with Fibers, retry/scheduling, Schema validation, Streams, and Sinks.
tools
Complete E2E (end-to-end) and integration testing skill for TypeScript/NestJS projects using Jest, real infrastructure via Docker, and GWT pattern. ALWAYS use this skill when user needs to: **SETUP** - Initialize or configure E2E testing infrastructure: - Set up E2E testing for a new project - Configure docker-compose for testing (Kafka, PostgreSQL, MongoDB, Redis) - Create jest-e2e.config.ts or E2E Jest configuration - Set up test helpers for database, Kafka, or Redis - Configure .env.e2e environment variables - Create test/e2e directory structure **WRITE** - Create or add E2E/integration tests: - Write, create, add, or generate e2e tests or integration tests - Test API endpoints, workflows, or complete features end-to-end - Test with real databases, message brokers, or external services - Test Kafka consumers/producers, event-driven workflows - Working on any file ending in .e2e-spec.ts or in test/e2e/ directory - Use GWT (Given-When-Then) pattern for tests **REVIEW** - Audit or evaluate E2E tests: - Review existing E2E tests for quality - Check test isolation and cleanup patterns - Audit GWT pattern compliance - Evaluate assertion quality and specificity - Check for anti-patterns (multiple WHEN actions, conditional assertions) **RUN** - Execute or analyze E2E test results: - Run E2E tests - Start/stop Docker infrastructure for testing - Analyze E2E test results - Verify Docker services are healthy - Interpret test output and failures **DEBUG** - Fix failing or flaky E2E tests: - Fix failing E2E tests - Debug flaky tests or test isolation issues - Troubleshoot connection errors (database, Kafka, Redis) - Fix timeout issues or async operation failures - Diagnose race conditions or state leakage - Debug Kafka message consumption issues **OPTIMIZE** - Improve E2E test performance: - Speed up slow E2E tests - Optimize Docker infrastructure startup - Replace fixed waits with smart polling - Reduce beforeEach cleanup time - Improve test parallelization where safe Keywords: e2e, end-to-end, integration test, e2e-spec.ts, test/e2e, Jest, supertest, NestJS, Kafka, Redpanda, PostgreSQL, MongoDB, Redis, docker-compose, GWT pattern, Given-When-Then, real infrastructure, test isolation, flaky test, MSW, nock, waitForMessages, fix e2e, debug e2e, run e2e, review e2e, optimize e2e, setup e2e