Headless virtualization
Massive scroll surfaces without massive DOM.
Virtual calculates the visible window for long lists, grids, and scroll containers so your app can keep the markup, layout, and design while rendering only the work the user can see.
00.0 MillionTotal Downloads000,000,000Weekly Downloads0GitHub StarsThe most popular and most used virtualization engine for modern web apps.
Visible window
render the viewport, not the dataset
Measured sizing
fixed, variable, dynamic rows
Headless scroll
own the container, markup, and styles
scroll API
virtualizer.scrollToIndex(180)range
Why Virtual
Virtualization is not just fewer nodes. It is scroll math, measurement, overscan, dynamic content, and container ownership. Virtual gives you those primitives without forcing a visual component on top.
Virtual calculates the visible range, pads the scroll space, and lets you render only the items the user can actually see.
Use fixed sizes when you can, measured dynamic sizes when content varies, and overscan to keep fast scrolling smooth.
Window scrolling, element scrolling, grids, lanes, sticky UI, and custom markup remain your responsibility and your freedom.
Pair Virtual with Table for giant grids, Query for paged data, Router for URL state, or your own renderer for anything else.
Tell the virtualizer how many things exist, even if most of them are not mounted.
Start from a stable size estimate so the scroll range is known immediately.
Let dynamic items report real sizes as content loads or expands.
Map virtual items to your own absolutely positioned rows, cells, or cards.
Virtualizer lifecycle
Estimate first, measure when needed, then render virtual items into your own row, card, cell, or lane components. The API is small because the layout remains yours.
AI chat virtualization
Chat, agents, copilots, logs, and support inboxes need end anchoring, stable prepends, append-follow, and a reliable way to jump back to the latest turn. Virtual now models those behaviors as scroll primitives instead of app-specific bookkeeping.
anchorTo
'end' keeps the latest edge stable
followOnAppend
follow only when already pinned
scrollToEnd
wire a Latest control to the API
measureElement
let streamed bubbles grow naturally
anchorTo: 'end'
272px from end / reading history
Can you summarize the alerts from the last deploy?
Three services reported higher latency, but only search crossed the user-visible threshold. I would start with the cache miss spike at 14:42.
query deploy_events --service search --window 30m
The slow requests line up with a schema warmup path. The good news: the regression is isolated and the route recovered after the cache filled.
Draft the follow-up for the incident channel.
Scroll surfaces
A virtualizer is a calculation layer. Whether the output becomes a vertical feed, a horizontal timeline, or a two-dimensional grid, the product keeps control of the actual experience.
Feeds, menus, logs, timelines, search results, and long admin indexes.
Calendars, kanban lanes, timelines, image strips, and dense inspectors.
Rows and columns that need windowing without adopting a canned grid UI.
Framework adapters
Use the adapter that fits your framework, or work from the core. The virtual range, measurements, and scroll behavior stay focused on the data and container instead of the renderer.
import { useVirtualizer } from '@tanstack/react-virtual'
const rowVirtualizer = useVirtualizer({
count: 1000,
getScrollElement: () => parentRef.current,
estimateSize: () => 36,
})
// Map virtual rows to your UIimport { useVirtualizer } from '@tanstack/react-virtual'
const rowVirtualizer = useVirtualizer({
count: 1000,
getScrollElement: () => parentRef.current,
estimateSize: () => 36,
})
// Map virtual rows to your UIimport { createVirtualizer } from '@tanstack/solid-virtual'
const parentRef: HTMLElement | undefined = undefined
const rowVirtualizer = createVirtualizer({
count: 1000,
getScrollElement: () => parentRef!,
estimateSize: () => 36,
})
// Map rowVirtualizer.getVirtualItems() to your UIimport { createVirtualizer } from '@tanstack/solid-virtual'
const parentRef: HTMLElement | undefined = undefined
const rowVirtualizer = createVirtualizer({
count: 1000,
getScrollElement: () => parentRef!,
estimateSize: () => 36,
})
// Map rowVirtualizer.getVirtualItems() to your UI<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const parentRef = ref<HTMLElement | null>(null)
const rowVirtualizer = useVirtualizer({
count: 1000,
getScrollElement: () => parentRef.value!,
estimateSize: () => 36,
})
</script>
<template>
<div ref="parentRef" style="overflow: auto; height: 300px">
<!-- Render rowVirtualizer.getVirtualItems() -->
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const parentRef = ref<HTMLElement | null>(null)
const rowVirtualizer = useVirtualizer({
count: 1000,
getScrollElement: () => parentRef.value!,
estimateSize: () => 36,
})
</script>
<template>
<div ref="parentRef" style="overflow: auto; height: 300px">
<!-- Render rowVirtualizer.getVirtualItems() -->
</div>
</template><script lang="ts">
import { createVirtualizer } from '@tanstack/svelte-virtual'
let parentRef: HTMLDivElement
const rowVirtualizer = createVirtualizer({
count: 1000,
getScrollElement: () => parentRef,
estimateSize: () => 36,
})
</script>
<div bind:this={parentRef} style="overflow:auto; height:300px">
<!-- Render $rowVirtualizer.getVirtualItems() -->
</div><script lang="ts">
import { createVirtualizer } from '@tanstack/svelte-virtual'
let parentRef: HTMLDivElement
const rowVirtualizer = createVirtualizer({
count: 1000,
getScrollElement: () => parentRef,
estimateSize: () => 36,
})
</script>
<div bind:this={parentRef} style="overflow:auto; height:300px">
<!-- Render $rowVirtualizer.getVirtualItems() -->
</div>import { LitElement, customElement, html } from 'lit'
import { createLitVirtualizer } from '@tanstack/lit-virtual'
@customElement('virtual-list')
export class VirtualList extends LitElement {
private parent?: HTMLDivElement
virtualizer = createLitVirtualizer({
count: 1000,
getScrollElement: () => this.parent!,
estimateSize: () => 36,
})
render() {
return html`<div style="overflow:auto; height:300px"></div>`
}
}import { LitElement, customElement, html } from 'lit'
import { createLitVirtualizer } from '@tanstack/lit-virtual'
@customElement('virtual-list')
export class VirtualList extends LitElement {
private parent?: HTMLDivElement
virtualizer = createLitVirtualizer({
count: 1000,
getScrollElement: () => this.parent!,
estimateSize: () => 36,
})
render() {
return html`<div style="overflow:auto; height:300px"></div>`
}
}import { Component, ElementRef, viewChild } from '@angular/core'
import { createAngularVirtualizer } from '@tanstack/angular-virtual'
@Component({
standalone: true,
selector: 'virtual-list',
template: '<div #parent style="overflow:auto; height:300px"></div>',
})
export class VirtualListComponent {
parent = viewChild.required<ElementRef<HTMLDivElement>>('parent')
virtualizer = createAngularVirtualizer(() => ({
count: 1000,
getScrollElement: () => this.parent().nativeElement,
estimateSize: () => 36,
}))
}import { Component, ElementRef, viewChild } from '@angular/core'
import { createAngularVirtualizer } from '@tanstack/angular-virtual'
@Component({
standalone: true,
selector: 'virtual-list',
template: '<div #parent style="overflow:auto; height:300px"></div>',
})
export class VirtualListComponent {
parent = viewChild.required<ElementRef<HTMLDivElement>>('parent')
virtualizer = createAngularVirtualizer(() => ({
count: 1000,
getScrollElement: () => this.parent().nativeElement,
estimateSize: () => 36,
}))
}Field notes
The existing copy had the right spirit: a tiny API for vertical, horizontal, and grid-style virtualization, with all the design control left in your hands.
See what teams are saying
"We chose TanStack Virtual for our virtualization needs - it handles our massive lists without breaking a sweat."
"TanStack Virtual is the answer when you need to render thousands of rows without destroying performance. Headless, flexible, and just works."
"For anyone dealing with large datasets in React, TanStack Virtual is a must. The row virtualizer alone saved our app."
"We chose TanStack Virtual for our virtualization needs - it handles our massive lists without breaking a sweat."
"TanStack Virtual is the answer when you need to render thousands of rows without destroying performance. Headless, flexible, and just works."
"For anyone dealing with large datasets in React, TanStack Virtual is a must. The row virtualizer alone saved our app."
Open source ecosystem
Maintainers, adapters, examples, partners, and GitHub sponsors keep the virtualization core close to real scrolling problems.