@tanstack/vue-virtual
Headless list/grid virtualization for Vue. The adapter is thin: useVirtualizer and useWindowVirtualizer construct a core Virtualizer, wire it to Vue reactivity, and return it as a Ref. Every option and method below comes from @tanstack/virtual-core, which the adapter re-exports (src/index.ts:21).
Version facts
- Target:
@tanstack/vue-virtual@3.13.39(package.json:3). - Exact dependency:
@tanstack/virtual-core@3.17.11(package.json:52). - Peer:
vue ^2.7.0 || ^3.0.0(package.json:59). - The adapter API has been stable across 3.13.x; releases 3.13.25 to 3.13.39 only bump virtual-core (vue-virtual changelog).
- Install:
npm install @tanstack/vue-virtual(installation docs).
Quick start: dynamic-height rows
Canonical pattern, following the official Vue dynamic example. Block translation positions the rendered block by the first item's start; each item flows normally inside it.
<script setup lang="ts">
import { computed, onMounted, onUpdated, ref, shallowRef } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const parentRef = ref<HTMLElement | null>(null)
const rows = Array.from({ length: 10000 }, (_, i) => `Row ${i}`)
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.value,
estimateSize: () => 55,
overscan: 5,
})
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
const itemEls = shallowRef<HTMLElement[]>([])
function measureAll() {
rowVirtualizer.value.measureElement(null) // prune disconnected nodes
itemEls.value.forEach((el) => el && rowVirtualizer.value.measureElement(el))
}
onMounted(measureAll)
onUpdated(measureAll)
</script>
<template>
<div ref="parentRef" style="height: 400px; overflow-y: auto; contain: strict">
<div :style="{ height: `${totalSize}px`, position: 'relative', width: '100%' }">
<div
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRows[0]?.start ?? 0}px)`,
}"
>
<div
v-for="virtualRow in virtualRows"
:key="virtualRow.key"
:data-index="virtualRow.index"
ref="itemEls"
>
{{ rows[virtualRow.index] }}
</div>
</div>
</div>
</div>
</template>
The scroll container needs a bounded height and overflow: auto. The inner sizer div carries getTotalSize().
Window virtualizer
useWindowVirtualizer scrolls with the browser window; it pre-fills getScrollElement (SSR-safe) and initialOffset, so pass neither (vue-virtual docs, src/index.ts:104-114). When the list sits below other content, measure that offset into scrollMargin:
const parentOffset = ref(0)
onMounted(() => {
parentOffset.value = parentRef.value?.offsetTop ?? 0
})
const rowVirtualizer = useWindowVirtualizer(
computed(() => ({
count: rows.length,
estimateSize: () => 45,
scrollMargin: parentOffset.value,
})),
)
Pattern source: official window example.
Vue adapter rules (proven by src/index.ts)
- Returns
Ref<Virtualizer>, not a raw instance. Use.valuein script; templates auto-unwrap (src/index.ts:30,69). - Options accept
MaybeRef. A plain object is applied once; to updatecount,overscan,enabled, etc. reactively, pass areforcomputedof the whole options object. The adapter watches() => unref(options)and callssetOptionson change (src/index.ts:48-65). getScrollElement()is watched withimmediate: true. A template ref that is stillnullon setup is picked up when it mounts (src/index.ts:36-46).onChangeis wrapped: the adapter callstriggerRefon its internalshallowRefbefore your callback, socomputed(() => rowVirtualizer.value.getVirtualItems())re-evaluates on scroll and resize (src/index.ts:53-56).- Observers are cleaned up via
onScopeDispose(src/index.ts:67). - Items measured with
measureElementmust carry the index attribute,data-indexby default (configurable viaindexAttribute). - Do not pass
observeElementRect,observeElementOffset, orscrollToFn; the adapter pre-fills them (src/index.ts:83-90).
Details: Vue adapter API.
API reference
Full options table, instance methods, and VirtualItem shape: Virtualizer API.
Required options for useVirtualizer: count, getScrollElement, estimateSize (virtualizer docs).
What is new since virtual-core 3.14.0
vue-virtual 3.13.39 ships core 3.17.11; these arrived via core minors (virtual-core changelog):
- 3.16.0: end-anchored mode for chat, logs, reverse feeds.
anchorTo: 'end',followOnAppend,scrollEndThreshold, plusscrollToEnd(),isAtEnd(),getDistanceFromEnd(). See Chat and end anchoring. - 3.15.0:
takeSnapshot()for scroll restoration round-trips withinitialMeasurementsCache+initialOffset. - 3.15.0: default scroll adjustment now skips compensation during backward scroll (fixes "items jump while scrolling up"); override by assigning the instance property
shouldAdjustScrollPositionOnItemSizeChange, which is not an option. - 3.15.0: iOS Safari momentum-scroll handling, default on; scroll compensation writes are deferred while touching or bouncing.
- 3.17.0:
useCachedMeasurementskeeps measurements alive while the list is hidden. - Pre-3.15 options still current:
laneswithlaneAssignmentMode(3.14.0),gap,indexAttribute,initialMeasurementsCache,isRtl.
Best practices
- Estimate large for dynamic items: set
estimateSizenear the maximum likely size so initial positions and the scrollbar stay stable (virtualizer docs). - Prefer block translation (as in the quick start) over per-item absolute positioning; smooth
scrollToIndexskips measuring items far from the target, and block translation keeps the rendered block internally consistent (virtualizer docs). - Give a stable
getItemKey(row id, not index) whenever data reorders, filters, prepends, or streams; index keys break end-anchored chat and scroll restoration (chat docs). - Subtract
scrollMarginfrom item starts when items are positioned absolutely inside a shared scroll container (virtualizer docs). - Use the
gapoption instead of CSS margins sogetTotalSize()accounts for spacing. countchanges invalidate the measurement cache and updategetTotalSize()automatically (fixed in 3.13.13, changelog); in Vue still route reactive data throughcomputedoptions.- Leave
useAnimationFrameWithResizeObserveroff unless measured to help; it defers measurements by a frame (virtualizer docs). - Pause observers with
enabled: falseinstead of unmounting to preserve measurements.
Recipes for masonry, padding, sticky headers, scroll restoration, and hidden lists: Patterns.
Migrating from v2: useVirtual is gone; v3 uses useVirtualizer, the count option (not size), getScrollElement (not parentRef), and measureElement refs with data-index (vue-virtual docs).
References
- Vue adapter API: signatures, reactivity contract, SSR behavior.
- Virtualizer API: all options, defaults, instance methods,
VirtualItem. - Patterns: masonry, scroll margin, sticky ranges, scroll restoration, hidden lists.
- Chat and end anchoring:
anchorTo: 'end'chat feeds in Vue.