VirtualList
The core component. Renders only the rows visible in the viewport plus an overscan buffer. ResizeObserver measures each row after mount so variable-height rows are handled automatically.
Props
items
T[]
Data array.
keyField
string · default: 'id'
Field used as the :key for each row.
estimatedItemSize
number | (item, index) => number · default: 50
Initial size estimate per row — height, or width when horizontal is set.
overscan
number · default: 3
Extra rows rendered above/below the viewport.
minHeight
number · default: 0
Minimum total list height in px (vertical mode).
minWidth
number · default: 0
Minimum total list width in px (horizontal mode).
scrollElement
HTMLElement | null · default: null
External scroll container (mutually exclusive with pageMode).
pageMode
boolean · default: false
Use window as the scroll container. Fixed at mount — see note below.
horizontal
boolean · default: false
Virtualize along scrollLeft/clientWidth instead of scrollTop/clientHeight. RTL-safe. Fixed at mount — see note below.
pageModeandhorizontalare read once at mount, like an axis/mode choice rather than a live-reactive prop. If your UI lets users togglehorizontalat runtime, bind:keyto the value driving it (e.g.:key="layout") so Vue remounts the component instead of leaving the previous axis wired up — see the VirtualList demo tab's Layout toggle for a working example.
isLoading
boolean · default: false
Shows the #skeleton slot when items is empty.
restoreKey
string · default: —
Key used to save/restore scroll position in sessionStorage.
ssrPreloadCount
number · default: 20
Number of rows rendered on the server.
recyclePool
boolean · default: false
Reuse DOM nodes instead of unmounting them (better scroll FPS, disables key-based transitions).
motionBlur
boolean · default: false
Apply a CSS blur that scales with scroll velocity, clearing ~150ms after scrolling settles.
containerRole
string · default: 'list'
ARIA role for the scroll container. A wrapping component (like VirtualTree or VirtualSelect) sets this to 'none' when it already owns the semantic role, so this element is removed from the accessibility tree instead of nesting one role inside another.
itemRole
string · default: 'listitem'
ARIA role for each row wrapper. Set to 'none' when the slot content itself renders the row's real role (e.g. treeitem, option), so the wrapper doesn't produce an invalid nested-role accessibility tree.
Slots
default
Scope: { item: T, index: number, style }
Row content.
empty
Scope: No scope
Rendered when items is empty and not loading.
skeleton
Scope: No scope
Rendered when items is empty and isLoading is true.
loading
Scope: No scope
Rendered at the bottom while isLoading is true.
Emits
scroll
Payload: Event
Native scroll event.
visible-range-change
Payload: { start: number; end: number }
Fires when the visible slice changes.
Exposed API (VirtualListExpose)
scrollTo(index, align?, options?)
Scroll to an item. align is 'start' | 'center' | 'end' | 'auto'. options.behavior is 'auto' (default, instant) or 'smooth' (native smooth-scroll animation).
scrollToOffset(px, options?)
Scroll to a raw pixel offset. Same options.behavior.
measureItem(index, height)
Manually set a row height.
getScrollElement()
Returns the element that actually scrolls — pair with VirtualScrollbar.
import type { VirtualListExpose } from 'vue-virtual-scroller-kit'
const listRef = ref<VirtualListExpose | null>(null)
listRef.value?.scrollTo(index, align) // 'start' | 'center' | 'end' | 'auto'
listRef.value?.scrollTo(index, 'start', { behavior: 'smooth' }) // native smooth-scroll animation
listRef.value?.scrollToOffset(px) // scroll to a pixel offset
listRef.value?.scrollToOffset(px, { behavior: 'smooth' })
listRef.value?.measureItem(index, height) // manually set a row height
listRef.value?.getScrollElement() // the element that actually scrolls — pair with VirtualScrollbar
behavior: 'smooth'defaults to'auto'(instant jump, unchanged from before). Smooth-scrolling to a far-off virtualized index targets the current estimated offset. A directscrollTopwrite (e.g. from the anchor-compensation described below) would otherwise cancel an in-progress native smooth-scroll, so that compensation is suppressed for ~1s after anybehavior: 'smooth'call.
Examples
Fixed-height rows:
<VirtualList :items="rows" :estimated-item-size="48" style="height: 500px">
<template #default="{ item }">
<div class="row">{{ item.name }}</div>
</template>
</VirtualList>Variable-height rows (per-item estimate):
<VirtualList
:items="posts"
:estimated-item-size="(item) => (item.isExpanded ? 200 : 60)"
style="height: 600px"
>
<template #default="{ item }">
<PostCard :post="item" />
</template>
</VirtualList>External scroll container:
<div ref="scrollEl" style="overflow-y: auto; height: 400px">
<VirtualList :items="rows" :scroll-element="scrollEl" :estimated-item-size="48">
<template #default="{ item }"><Row :data="item" /></template>
</VirtualList>
</div>Page-mode (whole page scrolls):
<VirtualList :items="rows" page-mode :estimated-item-size="80">
<template #default="{ item }"><Article :post="item" /></template>
</VirtualList>Skeleton loading state:
<VirtualList :items="items" :is-loading="isLoading" :estimated-item-size="56" style="height: 500px">
<template #default="{ item }"><Item :data="item" /></template>
<template #skeleton>
<SkeletonRow v-for="i in 10" :key="i" />
</template>
<template #empty>
<div>No items found.</div>
</template>
</VirtualList>Scroll restoration:
<VirtualList :items="rows" restore-key="my-list" :estimated-item-size="48" style="height: 500px">
<template #default="{ item }"><Row :data="item" /></template>
</VirtualList>DOM recycling pool (high-FPS heavy rows):
<VirtualList :items="rows" recycle-pool :estimated-item-size="80" style="height: 500px">
<template #default="{ item }"><HeavyRow :data="item" /></template>
</VirtualList>Note:
recyclePoolreuses DOM nodes so keyed Vue transitions on individual items won't work. Use it when render performance matters more than per-item animations.
Motion blur while scrolling fast:
<VirtualList :items="rows" motion-blur :estimated-item-size="48" style="height: 500px">
<template #default="{ item }">
<div class="row">{{ item.name }}</div>
</template>
</VirtualList>Horizontal layout (card strip):
<VirtualList horizontal :items="cards" :estimated-item-size="220" style="height: 240px">
<template #default="{ item }">
<div class="card" style="width: 220px; height: 100%">{{ item.title }}</div>
</template>
</VirtualList>In horizontal mode,
estimatedItemSizeis a width estimate and each row is positioned viainset-inline-start(RTL-safe) withheight: 100%— the slot content is responsible for its ownwidth(fixed, or measured dynamically viaResizeObserver, same as row heights in vertical mode). Pair with<VirtualScrollbar orientation="horizontal">for a themable scrollbar.