fn

useVirtualList

v0.0.14testeddemo

Virtualize a large list with dynamically measured item sizes. Rows render at their natural size: layout starts from estimateSize and is corrected by a shared ResizeObserver, which fires after layout but before paint, so corrections are not visible as flicker. Offsets come from a Fenwick tree (O(log n) hot paths). When an item above the viewport changes size — or the list is prepended to — the scroll position is compensated so content does not jump; the compensation write is deferred until after the DOM patch (still pre-paint) so it is never clamped by a stale wrapper height. Supports vertical and horizontal (LTR) layouts, gap/paddings, an external scrollElement, followOutput chat pinning, scrollTo with nearest-edge 'auto' alignment, and SSR via initialContainerSize. Non-goals (by design): window as scroller (element scrollers only), RTL horizontal mode, reactive options (only the source and scrollElement are reactive), pixel-perfect behavior: 'smooth' landings.

Examples

ts
const messages = shallowRef<Message[]>([]);
const { list, containerProps, wrapperProps } = useVirtualList(messages, {
  estimateSize: m => 52 + Math.ceil(m.text.length / 80) * 20,
  getItemKey: m => m.id, // measurements survive prepend/reorder
  followOutput: true,    // stay pinned to the newest message
});
// <div v-bind="containerProps" style="height: 300px">
//   <div v-bind="wrapperProps">
//     <article v-for="item in list" :key="item.key" v-bind="item.props">
//       {{ item.data.text }} <!-- natural height, measured automatically -->
//     </article>
//   </div>
// </div>
ts
// Fixed-size grid rows: the estimate is exact, measurement never corrects.
const { list } = useVirtualList(items, { estimateSize: 44, overscan: 10 });

Demo

Loading demo…

Signature

ts
export function useVirtualList<T = unknown>(
  source: MaybeRefOrGetter<readonly T[]>,
  options: UseVirtualListOptions<T> ={ ... }

Type Parameters

T= unknown

Parameters

ParameterTypeDescription
sourceMaybeRefOrGetter<readonly T[]>The full source array (may be reactive)
options?UseVirtualListOptions<T>Layout and behavior options

Returns

UseVirtualListReturn<T>{ list, totalSize, range, isScrolling, containerRef, containerProps, wrapperProps, measureElement, scrollTo, scrollToOffset, getOffsetForIndex, updateLayout, remeasure }
PropertyTypeDescription
listComputedRef<Array<UseVirtualListItem<T>>>Items in the current window, with layout offsets and spreadable props.
totalSizeComputedRef<number>Full content size along the scroll axis (paddings and gaps included), px.
rangeReadonly<ShallowRef<UseVirtualListRange>>Current rendered index window [start, end), overscan included.
isScrollingReadonly<ShallowRef<boolean>>
containerRefShallowRef<HTMLElement | null>Scroll container element — bind via containerProps or use directly.
containerPropsUseVirtualListContainerPropsProps to bind on the scrolling container element.
wrapperPropsComputedRef<{ style: CSSProperties }>Reactive props to bind on the inner wrapper (sizer) element.
measureElement(el: unknown) => voidRow measurer — already wired into item.props.ref; exposed for custom layouts.
scrollTo(index: number, options?: UseVirtualListScrollToOptions) => voidScroll the container so the item at index satisfies the alignment.
scrollToOffset(offset: number, options?: { behavior?: ScrollBehavior }) => void
getOffsetForIndex(index: number, align?: UseVirtualListAlign) => numberScroll offset that would satisfy align for index. Reflects the layout as of the last flush (does not force a re-sync).
updateLayout() => voidRe-read the source and rebuild layout. Only needed after in-place mutation of the source array (watch cannot observe those) — replacing the array triggers this automatically, and scrollTo re-syncs a same-tick replacement on its own.
remeasure(index?: number) => voidDrop cached measurements (all, or one index) and re-measure live rows. Useful when row content changes without a resize the observer would see.