Skip to content

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.

pageMode and horizontal are read once at mount, like an axis/mode choice rather than a live-reactive prop. If your UI lets users toggle horizontal at runtime, bind :key to 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.

ts
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 direct scrollTop write (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 any behavior: 'smooth' call.

Examples ​

Fixed-height rows:

vue
<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):

vue
<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:

vue
<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):

vue
<VirtualList :items="rows" page-mode :estimated-item-size="80">
  <template #default="{ item }"><Article :post="item" /></template>
</VirtualList>

Skeleton loading state:

vue
<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:

vue
<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):

vue
<VirtualList :items="rows" recycle-pool :estimated-item-size="80" style="height: 500px">
  <template #default="{ item }"><HeavyRow :data="item" /></template>
</VirtualList>

Note: recyclePool reuses 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:

vue
<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):

vue
<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, estimatedItemSize is a width estimate and each row is positioned via inset-inline-start (RTL-safe) with height: 100% — the slot content is responsible for its own width (fixed, or measured dynamically via ResizeObserver, same as row heights in vertical mode). Pair with <VirtualScrollbar orientation="horizontal"> for a themable scrollbar.