API Reference

Complete documentation for @pdanpdan/virtual-scroll.

Introduction

@pdanpdan/virtual-scroll renders only the rows near the viewport, so a list stays fast no matter how many items it holds. It scrolls vertically, horizontally, or on both axes at once (grids), handles fixed, computed and measured item sizes, and works with RTL layouts, the browser window as the scroll container, and lists that overflow the browser's own size limit. The live examples cover every feature; the reference below documents the full API.

Performance

Virtualization keeps the DOM small by rendering only the items in the viewport (plus a configurable buffer), so scrolling stays responsive however large the dataset. Scroll handling is tuned per sizing mode; the choices below have the largest effect.

Scroll performance & scrollbar dragging

Browsers throttle native scrollbar dragging: while the thumb is dragged, the scroll position advances in coarse per-frame steps. As a result, the content (and the virtualized items that follow it) can lag noticeably behind the thumb on large drags, even when the target area was already rendered. Wheel, touch, and keyboard scrolling are not affected.

To keep scrollbar dragging instant and 1:1 with the pointer, use the built-in virtual scrollbars:

  1. Enable them with the virtualScrollbar prop on VirtualScroll (they are also enabled automatically for lists beyond the browser size limit).
  2. Match your design using the --vs-scrollbar-* CSS variables, or take full control of the scrollbar UI with the scrollbar scoped slot.

The native scrollbar is hidden automatically (.virtual-scroll--hide-scrollbar) whenever virtual scrollbars are active - no extra CSS is needed.

Virtual scrollbars are not available when the scroll container is the window or the body - use an element container.

Other Performance Advice

  • Prefer fixed sizes. A numeric itemSize / columnWidth gives O(1) range math; arrays and functions use O(log n) Fenwick-tree lookups; dynamic (measured) sizes are the most expensive and re-measure with ResizeObserver. See the Sizing Guide below.
  • Skip per-row data for uniform lists. A numeric itemSize / columnWidth allocates no per-row storage and positions rows arithmetically, so index-only lists (sparse items, e.g. new Array(10_000_000)) keep memory flat at any scale - render row content from the slot's index instead of item.
  • Keep buffers modest. bufferBefore / bufferAfter (default 5) trade rendering cost for scrolling smoothness - a larger buffer means more DOM nodes and more work per frame.
  • Keep item content cheap. The item slot is re-rendered on scroll; avoid heavy markup, images, or effects inside items.
  • Use an element container for scrollable UIs instead of the window or body: it isolates scrolling, enables virtual scrollbars, and avoids full-page layout work.
  • Use ssrRange to pre-render the initial viewport and skip the first measure/scroll pass on slow devices.
  • Massive lists need no configuration. Past the browser's ~10,000,000 px limit the engine switches to coordinate scaling (virtual units mapped onto the display units the browser accepts) - except with window/body containers, where coordinate scaling and virtual scrollbars are unavailable.

Authoring Content for Virtualized Lists

Virtualization reuses a small window of DOM nodes: rows mount as they enter the viewport and unmount when they leave. Most authoring works exactly like any other Vue list, but content that relies on being mounted once, loads late, or grows after mount needs extra care to stay smooth and correct.

  • Keep row state in the model, not the DOM. Selection, expanded rows, likes, cart state, or carousel positions belong in your data (keyed by item id) or a store - never in the row's own DOM. Rows are recycled by index, so anything stored in the element disappears when the row scrolls away and would also leak across items.
  • Make row rendering idempotent. The item slot re-renders whenever the item enters the window (and again on scroll). Rendering the same item twice must produce the same result - no one-time setup, no listeners bound per mount that are never removed, no DOM the component does not own.
  • Prefer delegated or component-scoped events. Interactions on rows should bubble to the container or live in the item component's own handlers. State updates flow back into the model and the visible rows re-render from it - never mutate row content from outside.
  • Reserve space for media. Give images/videos an explicit width/height or aspect-ratio. Media that loads with unknown dimensions resizes the row after mount, which the engine measures and corrects - but repeated late growth causes visible jumps and extra work.
  • Do not combine native loading="lazy" with virtualization. The visible window is already the only mounted content; native lazy-loading adds browser heuristics on top of a scroll container whose content keeps changing. This can starve or delay the images on screen. Load visible images eagerly, or via your own bounded, low-priority prefetch window ahead of the viewport.
  • Prefetch offscreen content in bounded, deprioritised windows. If rows show remote data (images, fetched text), prefetch only a small range past the viewport and give it lower priority than what is visible, so on-screen content is never starved.
  • Avoid content that mounts asynchronously and changes row height late. Dynamic heights are supported (ResizeObserver measures and the layout corrects itself), but content whose size is stable or reserved up front scrolls smoother - especially in lists that also use snapping or sticky items.

The chat, gallery, blog, and data-browser examples in the playground demonstrate these patterns with real content: dynamic bubbles, media cards, grouped headers, and interactive rows.

Sizing Guide

Sizes can be a single number, a repeating pattern, a value computed per item, or measured from the DOM. Cheaper sizes mean less work per scroll, so pick the first one that fits your data.

TypeitemSize / columnWidthPerfDescription
FixednumberBestUniform size for all items. Calculations are O(1).
Array (Circular Pattern)number[]GreatRepeating size patterns from array (e.g. [50, 100]). O(log n).
Function(item, idx) => numberGoodKnown but variable sizes. No ResizeObserver overhead unless measured size differs.
Dynamic0, null, undefinedFairSizes measured via ResizeObserver after rendering.

Key Features

✓

Bidirectional Scrolling

Virtualize rows and columns together, for grids that scroll both ways.

✓

Dynamic Item Sizes

Rows measure themselves with ResizeObserver when their content changes.

✓

RTL Support

Detects RTL from the container and keeps horizontal positions correct.

✓

Native Window Scroll

Use the browser window/body as the scroll container.

✓

Sticky Headers/Footers

Headers and footers that stick and push each other, for grouped and sectioned lists.

✓

Scroll Restoration

Keeps the view in place when items are prepended - chat and live logs.

✓

SSR & Hydration

Pre-renders a range on the server and hydrates it in place.

✓

Massive List Support

Goes past the browser's ~10M px size limit, except with window/body containers.

✓

Virtual Scrollbars

Virtual scrollbars that stay 1:1 on huge lists; style them with CSS variables or your own markup.

✓

Scroll Snapping

Aligns to the start, center or end of an item when scrolling stops.

✓

Circular Sizing Patterns

Pass arrays to define repeating size patterns for items or columns.

✓

Masonry Layout

Masonry columns in a single scroll container: heights from your model, only the visible cards mounted.

Quick Start

Install the package:

pnpm add @pdanpdan/virtual-scroll

A lean entry is available for apps that only need virtualization. @pdanpdan/virtual-scroll/core ships the same API with the optional wiring compiled out - no keyboard navigation, custom scrollbars, snapping, sticky items, infinite loading or prepend restoration in VirtualScroll and VirtualScrollTable - together with its own smaller stylesheet:

import { VirtualScroll } from "@pdanpdan/virtual-scroll/core";
import "@pdanpdan/virtual-scroll/core/style.css";

In that build virtualScrollbar, snap, stickyIndices, loadDistance and restoreScrollOnPrepend are accepted but have no effect; loading still drives the loading slot and aria-busy. The flow-mode scrollbar a table shows for its own horizontal overflow stays. Use the package root when you need any of them.

Basic usage in a Vue component:

<script setup>
import { VirtualScroll } from "@pdanpdan/virtual-scroll";
import "@pdanpdan/virtual-scroll/style.css";

const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
</script>

<template>
<VirtualScroll :items="items" :item-size="50" class="h-96">
  <template #item="{ item }">
    <div class="h-12 flex items-center px-4 border-b border-base-200">
      {{ item.name }}
    </div>
  </template>
</VirtualScroll>
</template>

Usage Modes

Compiled Component

Recommended for most projects. Uses pre-compiled JavaScript.

import { VirtualScroll } from "@pdanpdan/virtual-scroll";
import "@pdanpdan/virtual-scroll/style.css";

  • Compatible with all modern bundlers.
  • Import the CSS yourself.

Original Vue SFC

Import the source and compile it with your own toolchain.

import VS from "@pdanpdan/virtual-scroll/VirtualScroll.vue";

  • Enables better tree-shaking in your build.
  • Styles handled by your Vue loader.

Headless Composable

You render the markup: the composable hands back the window to mount and the scroll state.

import { useVirtualScroll } from '@pdanpdan/virtual-scroll';

const props = computed(() => ({ items: items.value, itemSize: 50, hostRef: scrollEl.value }));
const { renderedItems, scrollDetails } = useVirtualScroll(props);

  • Render renderedItems yourself, at the offsets it returns.
  • Extensions go in the second argument - see Composables.

CDN Usage

Use directly in the browser without a build step.

<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pdanpdan/virtual-scroll/dist/virtual-scroll.css">
<script src="https://cdn.jsdelivr.net/npm/@pdanpdan/virtual-scroll/dist/index.js"></script>

  • No installation required.
  • Available via window.VirtualScroll.

Extensions

RTL, snapping, sticky items, infinite loading, scroll snapshots and coordinate scaling are extensions: each one hooks into the scroll lifecycle, so a plain list only carries what it needs and you opt into the rest. You can write your own against the same interface.

Built-in Extensions

Usage with Composable

When using the low-level useVirtualScroll composable:

import {
  useVirtualScroll,
  useRtlExtension,
  useSnappingExtension
} from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useRtlExtension(),
  useSnappingExtension()
]);

VirtualScroll Component

The component most projects use: pass your items and an item slot, and it mounts the rows around the scroll position, recycles them as you scroll, and keeps the scroll state in step - vertical, horizontal or grid, with the props and slots below.

Props

Core Configuration

PropTypeDefaultDescription
itemsT[]-The array of items to render. Required. Entries may be undefined (e.g. new Array(n) for index-only lists): every index in range renders and the slot item is undefined for holes; only the visible window is ever accessed.
itemSizenum | arr | fn | null40 (estimate)Fixed size, circular array pattern, or function. Omit it (or pass 0/null) to measure rows with a ResizeObserver; defaultItemSize is only the pre-measure estimate. See the Sizing Guide.
direction'vertical' | 'horizontal' | 'both''vertical'The scroll direction.
gapnumber0Spacing between items (vertical or horizontal).

Grid Configuration (only for direction="both")

PropTypeDefaultDescription
columnCountnumber0Number of columns for grid mode.
columnWidthnum | arr | fn | null100 (estimate)Width for columns in grid mode (fixed, array pattern, or function). Omit it (or pass 0/null) to measure columns; defaultColumnWidth is only the pre-measure estimate.
columnGapnumber0Spacing between columns.

Features & Behavior

PropTypeDefaultDescription
stickyIndicesnumber[][]Indices of items that should remain sticky. When stickyHeader or stickyFooter are enabled, they stick below or above them.
stickyHeader / stickyFooterbooleanfalseIf true, the header or footer size is measured and added to padding. Sticky stickyIndices items align below or above them.
ssrRange{start, end, ...}-Range of items to pre-render. See SSR Support.
loadingbooleanfalseWhile true, reveals the #loading slot (the slot stays mounted when provided and is hidden via CSS while false) and suppresses repeated load events.
loadDistancenumber200Distance from the end to trigger the load event.
snapbool | SnapModefalseAutomatically align to nearest item after scroll stops. See SnapMode.
virtualScrollbarbooleanfalseWhether to force the use of virtual scrollbars. Enabled automatically for massive lists, and disabled when window or body is the container.
restoreScrollOnPrependbooleanfalseMaintain scroll position when items are added to the top.
containerHTMLElement | WindowhostRefThe scrollable container. Defaults to the component's root element. Pass window or document.body to scroll the page.
initialScrollIndexnumber-Index to jump to on mount.
initialScrollAlignScrollAlignment | Options'start'Alignment for initial index.

Accessibility

PropTypeDefaultDescription
rolestring'list' | 'grid'ARIA role for the container. Automatically detected based on direction.
keyboardActivation'auto' | 'item' | 'viewport''auto'How the keyboard interacts with the content. 'auto' uses the roving item model for the roles that publish an active descendant (listbox, menu, tree) and viewport scrolling for everything else, including the grid role a two-axis list defaults to. 'item' always tracks an active item, 'viewport' never does. See Keyboard Navigation.
ariaLabelstring-Accessible label for the scroll container.
ariaLabelledbystring-ID of the element that labels the scroll container.
itemRolestring-ARIA role for each item. Set to 'none' to manually apply roles using getItemAriaProps.

ScrollAlignment

Controls the item's final position in the viewport: 'start' | 'center' | 'end' | 'auto'.

Advanced & Performance

PropTypeDefaultDescription
containerTagstring'div'HTML tag for the scroll container, e.g. for semantic list markup. For tabular data use the VirtualScrollTable component.
wrapperTagstring'div'HTML tag for the items wrapper. Combine 'ul'/'ol' with itemTag: 'li' for semantic lists. Tables should use the VirtualScrollTable component.
itemTagstring'div'HTML tag for each virtualized item (e.g. 'li'). For table rows use VirtualScrollTable.
headerTagstring'div'HTML tag for the header slot wrapper (e.g. 'header'). Tables use VirtualScrollTable, whose header slot renders the <thead>.
footerTagstring'div'HTML tag for the footer slot wrapper (e.g. 'footer'). Tables use VirtualScrollTable, whose footer slot renders the <tfoot>.
scrollPaddingStart / Endnum | {x, y}0Additional padding for scroll offsets.
bufferBefore / bufferAfternumber5Number of items to render outside the viewport.
defaultItemSizenumber40Estimated size for items before measurement.
defaultColumnWidthnumber100Estimated width for columns before measurement.
debugbooleanfalseEnables debug mode (visible offsets and indices).
* For a full list of props including advanced configuration, see the VirtualScrollProps interface.

Accessibility (ARIA)

Roles and attributes are set for you, so screen readers can follow the virtualized content: lists, grids, and also tree, listbox and menu roles.

With keyboardActivation in the item model - 'auto' selects it for listbox, menu and tree - the container publishes aria-activedescendant pointing at the active item, and a polite live region announces the active position (Item 21 of 100) as it moves. Enter/Space and handleItemActivate(index) emit itemActivate.

Role PropDefault Item RoleBehavior
list (default)listitemStandard 1D list.
gridrow2D data grid or table.
treetreeitemHierarchical structure.
listboxoptionSelectable list.
menumenuitemNavigation menu.

Slots

#item

Scoped slot for individual items.

  • item: T: The data item from the source array (undefined for holes in sparse or index-only datasets).
  • index: number: The original 0-based index of the item.
  • isSticky: boolean: true if the item is configured to be sticky via stickyIndices.
  • isStickyActive: boolean: true if the item is currently stuck at the threshold.
  • isStickyActiveX / Y: boolean: true if the item is stuck at the horizontal or vertical threshold.
  • isActive: boolean: true for the item the keyboard navigation currently tracks as active (see Keyboard Navigation). Nothing is active until the item model activates an item - the first arrow press does that - or setActiveIndex / handleItemActivate select one explicitly.
  • offset: { x, y }: Calculated physical position in display units (DU).
  • columnRange: ColumnRange: Precise indices and paddings for visible columns.
  • getColumnWidth: (index: number) => number: Helper to get the calculated width of any column.
  • getItemAriaProps: (index: number) => object: Helper to get ARIA attributes for an item (e.g. role="listitem", aria-posinset).
  • getCellAriaProps: (index: number) => object: Helper to get ARIA attributes for a cell (e.g. role="gridcell", aria-colindex).
  • gap: number: Vertical gap between items.
  • columnGap: number: Horizontal gap between columns.

#scrollbar

Scoped slot for custom scrollbar implementation.

  • axis: 'vertical' | 'horizontal': The scrollbar axis.
  • positionPercent: number: Current scroll position (0 to 1).
  • viewportPercent: number: Viewport as percentage of total size.
  • thumbSizePercent: number: Calculated thumb size (0 to 100).
  • thumbPositionPercent: number: Calculated thumb position (0 to 100).
  • trackProps: object: Attributes and listeners for the track element.
  • thumbProps: object: Attributes and listeners for the thumb element.
  • isDragging: boolean: Whether the thumb is currently being dragged.
  • scrollbarProps: object: Grouped properties for VirtualScrollbar.
    • axis: 'vertical' | 'horizontal'
    • totalSize: number
    • position: number
    • viewportSize: number
    • scrollToOffset: (offset: number) => void
    • containerId: string
    • isRtl: boolean
    • ariaLabel: string

#header / #footer

Content rendered above or below the virtualized items. Can be made sticky using the stickyHeader / stickyFooter props.

#loading

Always rendered when provided - hidden via the virtual-scroll-loading--hidden class (visibility: hidden) while loading is false - so it reserves its space and the End key can include its size in the scroll target. While loading is true, further load events are suppressed. Only provide the slot while a load is expected: once there is no more data, stop passing it (e.g. v-if="hasMore" on <template #loading>) so the reserved space disappears.

ScrollbarSlotProps

Properties passed to the 'scrollbar' scoped slot.

<template>
<VirtualScroll :items="items" direction="both" virtual-scrollbar>
  <template #scrollbar="{ trackProps, thumbProps, axis }">
    &lt;!-- Vertical Track -->
    <div v-if="axis === 'vertical'" v-bind="trackProps" class="w-2 bg-base-300">
      <div v-bind="thumbProps" class="bg-primary rounded" />
    </div>

    &lt;!-- Horizontal Track -->
    <div v-else v-bind="trackProps" class="h-2 bg-base-300">
      <div v-bind="thumbProps" class="bg-secondary rounded" />
    </div>
  </template>
</VirtualScroll>
</template>
PropertyTypeDescription
axis'vertical' | 'horizontal'The scrollbar axis.
positionPercentnumberScroll position percentage (0-1).
viewportPercentnumberViewport percentage of total (0-1).
thumbSizePercentnumberCalculated thumb size percentage (0-100).
thumbPositionPercentnumberCalculated thumb position percentage (0-100).
trackPropsRecord<string, unknown>Attributes/listeners for the track. Bind with v-bind="trackProps". Includes class and style.
thumbPropsRecord<string, unknown>Attributes/listeners for the thumb. Bind with v-bind="thumbProps". Includes class and style.
scrollbarPropsVirtualScrollbarPropsGrouped props for the VirtualScrollbar component: axis, totalSize, position, viewportSize, scrollToOffset, containerId, isRtl, ariaLabel. Useful for <VirtualScrollbar v-bind="scrollbarProps" />.
isDraggingbooleanWhether the thumb is currently being dragged.

Events

EventPayloadDescription
scrollScrollDetails<T>Emitted on every scroll position change.
load'vertical' | 'horizontal', LoadDetailsEmitted when the scroll position comes within loadDistance of the end on that axis; suppressed while loading is true and while the axis is still flinging faster than flingVelocity. The second argument carries { velocity, direction } - the scroll velocity in VU/ms and the travel direction ('start' | 'end' | null).
itemActivateindex: number, item: T | undefinedEmitted when the active item is activated with Enter/Space, or from an explicit handleItemActivate(index) call (e.g. a click in the item slot). Only emitted in the item activation model.
visibleRangeChange{ start, end, colStart, colEnd }Emitted when the set of rendered indices changes.

Keyboard Navigation

The container is keyboard-accessible when focused (tabindex="0"). What the keys do depends on the activation model chosen by the keyboardActivation prop: with 'viewport' they move the scroll position only and no item is tracked, while with 'item' they move a roving active item, keep it in view and announce it through a polite live region (for example Item 21 of 100). The 'auto' default picks the item model for the roles that publish an active descendant (listbox, menu, tree) and viewport scrolling for everything else. See Props.

Viewport scrolling ('viewport'):

HomeScroll to the start (Index 0,0).
EndScroll to the last row and column, including the loading slot size when a #loading slot is present.
PgUp / PgDnScroll by one full viewport: target is the first visible item minus one / the last visible item plus one.
↑↓Scroll vertically by item height (respects snap mode).
←→Scroll horizontally by column width (respects snap mode).

Roving active item ('item'):

↑↓←→Move the active item by one along the scroll axis and scroll it back into view only when it left the viewport. The first press activates the first visible item without scrolling. In grid mode the active item is a row, so the inline arrows keep panning columns.
Home / EndMove the active item to the first or last item and keep it visible; the other axis is left untouched, so a multi-column list keeps its current column.
PgUp / PgDnMove the active item one viewport up or down and keep it visible.
Enter / SpaceActivate the active item, emitting itemActivate. The same happens when you call handleItemActivate(index), e.g. from a click in the item slot.

CSS Classes

ClassDescription
.virtual-scroll-containerThe root scrollable container element.
.virtual-scroll-wrapperWraps rendered items and provides total scrollable dimensions.
.virtual-scroll-itemApplied to each individual rendered item. Use for general item styling.
.virtual-scroll-header / .virtual-scroll-footerContainers for header and footer slots.
.virtual-scroll-loadingContainer for the loading slot.
.virtual-scroll-loading--hiddenApplied to the loading slot while loading is false: hides it with visibility: hidden while keeping its space (the slot is always rendered when provided).
.virtual-scroll--vertical / --horizontal / --bothDirection modifiers applied to the root container.
.virtual-scroll--hydratedApplied after client-side mount and hydration is complete.
.virtual-scroll--windowApplied when scrolling via the global window object.
.virtual-scroll--tableApplied by the VirtualScrollTable component.
.virtual-scroll--stickyApplied to items that are currently stuck to the viewport edge.
.virtual-scroll--debugVisible when debug prop is active.
.virtual-scroll--hide-scrollbarApplied when virtual scrollbars are enabled or content is massive.

CSS Variables

The default VirtualScrollbar can be styled using the following CSS variables:

VariableDefault (Light/Dark)Description
--vs-scrollbar-bgrgba(230,230,230,0.9) / rgba(30,30,30,0.9)Track background color.
--vs-scrollbar-thumb-bgrgba(0,0,0,0.3) / rgba(255,255,255,0.3)Thumb background color.
--vs-scrollbar-thumb-hover-bgrgba(0,0,0,0.6) / rgba(255,255,255,0.6)Thumb background on hover/active.
--vs-scrollbar-size8pxWidth (vertical) or height (horizontal) of the scrollbar.
--vs-scrollbar-radius4pxBorder radius for track and thumb.
--vs-scrollbar-cross-gapvar(--vs-scrollbar-size)Size of gap to use where scrollbars meet.
--vs-scrollbar-has-cross-gap0If gap should be shown where scrollbars meet.

Exposed Members

The reactive state and methods available through a template ref; the instance type is VirtualScrollInstance<T>.

Properties

Methods

VirtualScrollTable Component

For tabular data, use the dedicated VirtualScrollTable component: it renders real <table>/<tbody>/<tr> markup, keeps the virtual offsets with spacer rows in table flow (flowTable) or with absolutely positioned rows, measures dynamic row heights, and provides the header, footer and item slots. A table wider than its container gets its own horizontal virtual scrollbar. See the Flow Table example and the Table example. Scroll snapping is supported in table mode: flow rows and absolute rows snap to the same row offsets as list mode.

Props

PropTypeDefaultDescription
flowTablebooleanfalseRender rows in real table flow between spacer rows instead of absolutely positioning them. Vertical lists only; row heights may be uniform (itemSize) or dynamic (measured). Unsupported configurations fall back to absolute rows.
autoSizeColumnsbooleanfalsePin column widths from the first rendered window via a colgroup with table-layout: fixed, so later windows never reflow the columns. Requires flowTable and equal direct-cell counts across rows.
columnWidthsnumber[]-Explicit column widths (px) pinned via the colgroup; takes precedence over autoSizeColumns.
stickyHeader / stickyFooterbooleanfalseMeasure and reserve the header/footer slots; the row groups stick to the viewport edges.
virtualScrollbarbooleanfalseForce virtual scrollbars; vertical and, when the table overflows horizontally, horizontal bars are rendered.

Shared API with VirtualScroll

VirtualScrollTable is the same component with table markup - everything on VirtualScroll still applies unless noted here:

  • Slots: the same item, header, footer, loading and scrollbar slots. The item slot receives the same scoped props (item, index, sticky flags, offsets, column range helpers). In table mode the slot content is rendered inside the row elements provided by the component, so items slot in <td> cells and header/footer slot in <th>/<td> cells. See Slots.
  • Events: identical scroll, visibleRangeChange, load and itemActivate events (a table tracks an active row only with keyboardActivation: 'item'). See Events.
  • Exposed instance (via ref): the same methods and state - scrollToIndex, scrollToOffset, refresh, updateItemSizes, stopProgrammaticScroll, scrollDetails, isHydrated, getItemOffset/getItemSize and the rest - plus table constants (isTable: true, itemTag: 'tr', containerTag: 'table', wrapperTag: 'tbody') and the table-only props it exposes (flowTable, autoSizeColumns, columnWidths) - not the tag props. The instance type is VirtualScrollTableInstance<T>. See Methods.
  • Props: the shared base surface applies unchanged - items, itemSize (uniform or dynamic), bufferBefore/bufferAfter, initialScrollIndex/initialScrollAlign, restoreScrollOnPrepend, infiniteScroll-related props (loadDistance, loading), ssrRange, debug, role/ARIA props and rtl. Tag customization (containerTag/wrapperTag/itemTag) lives on VirtualScroll for semantic lists (e.g. ul/ol > li). On VirtualScrollTable the container, wrapper and row elements are fixed to their semantic table tags - use this component for tabular data. Grid/gap/sticky-index/scroll-padding props are not part of the table flow surface (they fall back to absolute rows), and direction is vertical. See Props.
  • Behavior & theming: the same engine wiring - keyboard navigation, coordinate scaling, custom scrollbar slot support, sticky measurements, prepend restoration - and the same CSS classes and CSS variables. The snap caveat above applies.

VirtualScrollMasonry Component

VirtualScrollMasonry lays out a masonry grid inside a single native scroll container: columns are sized from the container width, each card goes to the shortest column, and only the cards around the scroll position are mounted - the DOM stays bounded however large the dataset, and a far scrollToIndex never mounts the cards it jumps over.

Heights come from the deterministic itemHeight oracle by default, so the layout is exact without measuring anything: jumps land on the real position and the total height is known as soon as the layout reaches the end of the list. With measuredHeights, mounted cards are measured instead and their real boxes drive the layout. When the container reflows (resize, different column geometry, a new dataset) the topmost visible card keeps its place on screen instead of the view jumping. See the Masonry example.

Props

PropTypeDefaultDescription
itemsT[]RequiredArray of data items to virtualize. May be sparse (new Array(n)): holes render and the slot item is undefined for them; only the rendered window is accessed.
itemHeightfn(item, index, columnWidth)RequiredCanonical height oracle in px. MUST be deterministic - the same (index, columnWidth) must always return the same height - because placements are committed to a frontier chain and replayed from stored snapshots. Non-finite results fall back to 40; finite non-positive results clamp to 1.
targetColumnWidthnumber240Desired column width in px. The column count is derived from the container width so columns land as close as possible to this target; the actual width is fractional so the gutters divide the width exactly.
minColumns / maxColumnsnumber1 / 10Column count bounds for the responsive reflow.
measuredHeightsbooleanfalseMeasure mounted cards with a ResizeObserver and drive the layout from the measured boxes instead of the oracle. Off: canonical oracle layout, nothing is measured. On: cards size to their content (the oracle height becomes the pre-measure minimum, so estimate-sized first mounts do not re-flow) and every accepted measurement re-lays-out with the topmost visible card re-anchored.
gapnumber10Spacing between cards in px, applied both between columns and between rows of the layout.
segmentSizenumber500Items per layout segment - the cadence at which the real column frontier is snapshotted. Larger segments store less frontier state but make each layout step cross more items.
virtualScrollbarbooleantrueRender the overlay virtual scrollbar (the native one is hidden while enabled). Hidden automatically when the content fits the viewport.
role / itemRolestring'list' / 'listitem'ARIA roles for the cards wrapper and each card (grid/tree/listbox/menu wrappers map to their child roles). Set itemRole: 'none' to disable role assignment.
ariaLabel / ariaLabelledbystring-Accessible label for the scroll container (the container role becomes region when labelled).
debugbooleanfalseOutline rendered card bounds and overlay a geometry badge (#index (x, y)) per card.

Item Slot

PropTypeDescription
item / indexT | undefined / numberThe original data item and its 0-based dataset index (undefined for sparse holes).
columnnumberThe 0-based column the card was placed into.
x / ynumberCard offset in px relative to the cards wrapper (the component positions the card itself via translate).
width / heightnumberCard size in px - the resolved column width and the layout-resolved height (oracle height in canonical mode; measured height with measuredHeights). In canonical mode render content to exactly fill the oracle height; with measuredHeights cards size to their content.

Exposed Members & Events

Available through a template ref; the instance type is VirtualScrollMasonryInstance<T>.

MemberTypeDescription
scroll (event)MasonryScrollDetails<T>Emitted on scroll and every layout change: rendered items (card geometry), currentIndex/currentEndIndex, range, scrollOffset/displayScrollOffset (y), viewportSize, totalSize, columnRange and scrolling flags.
scrollDetailsMasonryScrollDetails<T>Current scroll state (same shape as the scroll event payload).
columns / columnWidthnumberLive resolved column count and column width in px (0 until the container is measured) - e.g. for srcset candidates or text budgets.
totalHeight / totalHeightExactnumber / booleanContent height in px - extrapolated from the known frontier prefix until the chain reaches the end, then exact. End-anchored scrolls re-clamp as estimates settle.
scrollToIndexfn(index?, options?)Scroll to a card with align ('start' | 'center' | 'end' | 'auto'), behavior and dryRun; far jumps land on the exact canonical position.
scrollToOffsetfn(offset?, options?)Scroll to a pixel offset (use ±Infinity for the end/start; end intents follow the content as totals settle).
refreshfn()Drop every cached frontier and re-layout from the current anchor - after in-place item edits or oracle changes.

Sizing contract & limitations

  • In canonical mode cards must render at exactly the oracle height - reserve media space (aspect-ratio, fixed model heights) and never rely on DOM measurement. With measuredHeights, cards size to their content and only mounted cards are measured (unmounted regions keep the oracle estimate).
  • Vertical axis only: no RTL, horizontal, or both mode and no coordinate scaling, so datasets stay below the browser's ~10M px scroll limit.
  • Not available for SSR pre-rendering: content mounts after the container is measured. Extensions/snap/sticky/loading of the list engine do not apply.

VirtualScrollbar Component

An overlay scrollbar that looks and behaves the same in every browser, usable on its own or inside VirtualScroll. See the Independent Scrollbars example for it working without any virtualization.

<script setup>
import { VirtualScrollbar } from "@pdanpdan/virtual-scroll";
import { ref } from "vue";

const scrollX = ref(0);
const scrollY = ref(0);
</script>

<template>
<div class="relative overflow-hidden h-96">
  &lt;!-- Vertical Scrollbar -->
  <VirtualScrollbar
    axis="vertical"
    :total-size="10000"
    :viewport-size="400"
    :position="scrollY"
    @scroll-to-offset="val => scrollY = val"
  />

  &lt;!-- Horizontal Scrollbar -->
  <VirtualScrollbar
    axis="horizontal"
    :total-size="10000"
    :viewport-size="800"
    :position="scrollX"
    @scroll-to-offset="val => scrollX = val"
  />
</div>
</template>

Props

PropTypeDefaultDescription
axis'vertical' | 'horizontal''vertical'The axis of the scrollbar. Defaults to 'vertical'.
totalSizenumber-Total size of the scrollable content in pixels. Required.
viewportSizenumber-Size of the visible viewport in pixels. Required.
positionnumber-Current scroll position in pixels. Required.
scrollToOffset(offset: number) => void-Optional callback invoked with the new offset on user interaction, right before the scrollToOffset event fires.
containerIdstringundefinedID of the container element for accessibility.
isRtlbooleanfalseWhether the scrollbar is in Right-to-Left (RTL) mode.
ariaLabelstring-Accessible label for the scrollbar.

Events

EventPayloadDescription
scroll-to-offsetnumberEmitted when the user interacts with the scrollbar to change position.

Composables

Everything below is imported from the package root, with one exception: useVirtualScrollSizes is the sizing layer the engine builds on and lives in @pdanpdan/virtual-scroll/internal. The pure calculation layer (calculate*), the DOM scroll helpers, the masonry layout engine and their parameter bags live in that same entry: they are what the components are built on and they carry no compatibility guarantee - shapes and signatures may change in any release, including a patch. FenwickTree, the structure the sizing layer builds on, is exported from the root.

useVirtualScroll

The engine behind the component, for when you want to drive the markup yourself or build your own wrapper.

import { useVirtualScroll } from '@pdanpdan/virtual-scroll';
import { computed, ref } from 'vue';

const items = ref([...]);
const props = computed(() => ({
items: items.value,
itemSize: 50,
direction: 'vertical'
}));

const {
renderedItems,
scrollDetails,
totalHeight,
scrollToIndex
} = useVirtualScroll(props);

Parameters

Accepts a MaybeRefOrGetter to a VirtualScrollProps object.

Return Value

MemberTypeDescription
renderedItemsRef<RenderedItem<T>[]>List of items to render in the current buffer.
scrollDetailsRef<ScrollDetails<T>>Full reactive state of the virtual scroll system.
columnRangeRef<ColumnRange>Visible columns and their associated paddings.
totalWidth / totalHeightRef<number>Calculated total size of the scrollable content area (DU).
renderedWidth / renderedHeightRef<number>Total dimensions to be rendered in the DOM (clamped to browser limits, DU).
isHydratedRef<boolean>true when the component is mounted and hydrated.
isRtlRef<boolean>true if the scroll container is in Right-to-Left mode.
scaleX / scaleYRef<number>Current coordinate scaling factors (VU / DU).
componentOffset{ x: Ref<number>, y: Ref<number> }Absolute offset of the component in its container (DU).
scrollbarOffsetReactive<{ x: number; y: number }>Inline-start/block-start padding of the scroll container (DU), used to align the virtual scrollbar overlay with the scrollport.
activeIndexRef<number>Index of the item tracked by keyboard navigation, -1 when none.
setActiveIndexFunctionSets (null clears) the active item without scrolling, so a click or an external selection can be synced in.
handleItemActivateFunctionMarks an item active and calls onActivate (emits itemActivate on the component).
scrollToIndexFunctionProgrammatic scroll to a specific index. End-anchored scrolls re-clamp while settling measurements move the real end, so a first jump to the end lands flush on dynamic lists.
scrollToOffsetFunctionProgrammatic scroll to a pixel offset.
stopProgrammaticScrollFunctionCancel any active smooth scroll animation.
handleScrollCorrectionFunctionAdjust scroll position to compensate for measurement changes.
scrollCorrectionRef<Point>Correction the engine applied to the scroll position when sizes above the window changed; extensions and drag emulation use it to keep their own origin in sync.
refreshFunctionResets all measurements and state.
updateItemSizeFunctionRegister a manual item measurement.
updateItemSizesFunctionRegister multiple manual item measurements.
updateHostOffsetFunctionForce update the container's relative position.
updateDirectionFunctionManually trigger direction (LTR/RTL) detection.
getColumnWidthFunctionHelper to get a column's width.
getRowHeightFunctionHelper to get a row's height.
getRowOffsetFunctionHelper to get a row's virtual offset (VU).
getColumnOffsetFunctionHelper to get a column's virtual offset (VU).
getItemOffsetFunctionHelper to get an item's virtual offset (VU).
getItemSizeFunctionHelper to get an item's size along scroll axis (VU).
getRowIndexAtFunctionHelper to get the row (or item) index at a vertical virtual offset (VU).
getColumnIndexAtFunctionHelper to get the column index at a horizontal virtual offset (VU).

useVirtualScrollMasonry

The engine behind VirtualScrollMasonry. It derives the column geometry from the container width, places each card on the shortest column, mounts only the cards near the scroll position, and keeps the topmost visible card in place across relayouts. Headless users provide their own scrollable element through the hostRef prop.

import { useVirtualScrollMasonry } from '@pdanpdan/virtual-scroll';
import { computed, ref } from 'vue';

const items = ref([...]);
const hostRef = ref<HTMLElement | null>(null);
const props = computed(() => ({
  items: items.value,
  itemHeight: (item, index, width) => item.aspect * width,
  hostRef: hostRef.value
}));

const {
  renderedCards,
  scrollDetails,
  columns,
  columnWidth,
  totalHeight,
  scrollToIndex
} = useVirtualScrollMasonry(props);

Accepts a MaybeRefOrGetter to a VirtualScrollMasonryProps object (the component prop set plus hostRef). Returns renderedCards, scrollDetails, columns, columnWidth, totalHeight, totalHeightExact, scrollToIndex, scrollToOffset, refresh and the reactive internalState (scrollY, viewport size, scrolling flags) - see the component members for the semantics of each member.

useVirtualScrollSizes

Keeps track of item sizes: prefix sums, size updates and the scroll corrections that follow a measurement.

Part of the engine layer: import it from @pdanpdan/virtual-scroll/internal. It is not covered by semver - shapes and signatures may change in any release.

import { useVirtualScrollSizes } from '@pdanpdan/virtual-scroll/internal';
import { computed } from 'vue';

const {
  itemSizesY,
  updateItemSizes,
  getSizeAt
} = useVirtualScrollSizes(computed(() => ({
  props: { items: [], itemSize: 50 },
  isDynamicItemSize: false,
  isDynamicColumnWidth: false,
  defaultSize: 50,
  fixedItemSize: 50,
  direction: 'vertical'
})));

Parameters

Accepts a MaybeRefOrGetter to a UseVirtualScrollSizesProps object.

Return Value

MemberTypeDescription
itemSizesX / YFenwickTreePrefix sum trees for item sizes.
columnSizesFenwickTreePrefix sum tree for column widths.
measuredItemsX / YRef<Uint8Array>Bitmask of measured items.
measuredColumnsRef<Uint8Array>Bitmask of measured columns.
treeUpdateFlagRef<number>Reactive flag that increments when trees update.
sizesInitializedRef<boolean>True after initial sizes are calculated.
getItemBaseSizeFunctionHelper to get item size from props.
getSizeAtFunctionHelper to get current size at index.
initializeSizesFunctionSetup trees from component props.
updateItemSizesFunctionBatch register measurements and trigger corrections.
refreshFunctionReset all measurements and state.

useVirtualScrollbar

Scrollbar interactions: track clicks, thumb dragging and mapping between scrollbar and scroll positions (including RTL).

import { useVirtualScrollbar } from '@pdanpdan/virtual-scroll';
import { ref } from 'vue';

const scrollPos = ref(0);

const {
  trackProps,
  thumbProps,
  thumbSizePercent,
  thumbPositionPercent
} = useVirtualScrollbar(() => ({
  axis: 'vertical',
  totalSize: 10000,
  viewportSize: 500,
  position: scrollPos.value,
  scrollToOffset: (val) => { scrollPos.value = val; }
}));

Parameters

Accepts a MaybeRefOrGetter to a UseVirtualScrollbarProps object.

Return Value

MemberTypeDescription
trackPropsComputedRef<object>Attributes and listeners for the track element. Includes class and style.
thumbPropsComputedRef<object>Attributes and listeners for the thumb element. Includes class and style.
viewportPercentComputedRef<number>Viewport size as percentage of total size (0-1).
positionPercentComputedRef<number>Scroll position as percentage of scrollable range (0-1).
thumbSizePercentComputedRef<number>Calculated thumb size (percentage of track, 0-100).
thumbPositionPercentComputedRef<number>Calculated thumb position (percentage of track, 0-100).
isDraggingRef<boolean>Whether the thumb is currently being dragged.

useVirtualScrollInertia

Pointer dragging, inertia and wheel handling for the cases where native scrolling is not available (scaled lists, custom scrollbars).

import { useVirtualScrollInertia } from '@pdanpdan/virtual-scroll';

const {
  isPointerScrolling,
  handlePointerDown,
  handlePointerMove,
  handlePointerUp,
  handleWheel
} = useVirtualScrollInertia({
  useVirtualScrolling: ref(true),
  scrollDetails,
  scrollToOffset: (x, y) => { /* ... */ },
  stopProgrammaticScroll: () => { /* ... */ }
});

Parameters

Accepts an UseVirtualScrollInertiaOptions object.

Return Value

MemberTypeDescription
isPointerScrollingRef<boolean>True when user is actively dragging the content.
handlePointerDown / handlePointerMove / handlePointerUpFunctionPointer event handlers to be bound to the scroll container.
handleWheelFunctionWheel event handler to be bound to the scroll container.
shiftOriginFunctionMoves the origin a drag measures from, so a measurement correction that shifted the content is not undone by the next pointer move. No-op while no drag is in progress.
stopInertiaFunctionImmediately stops any active momentum animation.

useVirtualScrollKeyboard

Keyboard navigation for the container: Arrows, Home, End, PageUp and PageDown - either scrolling the viewport or moving a roving active item, depending on the activationMode option.

import { useVirtualScrollKeyboard } from '@pdanpdan/virtual-scroll';

const {
  handleKeyDown,
  activeIndex,
  liveMessage,
  setActiveIndex,
  handleItemActivate
} = useVirtualScrollKeyboard({
  props,
  scrollDetails,
  scrollToIndex: (row, col, opt) => { /* ... */ },
  scrollToOffset: (x, y, opt) => { /* ... */ },
  stopProgrammaticScroll: () => { /* ... */ },
  getLoadingSlotSize: () => loadingEl?.offsetHeight ?? 0, // optional
  activationMode: 'viewport', // 'viewport' (default) | 'item' - a ref or getter is accepted
  onActivate: (index) => { /* ... */ }, // Enter/Space, or handleItemActivate
  // ... resolvers
});

Parameters

Accepts an UseVirtualScrollKeyboardOptions object.

  • scrollToOffset: Scrolls to a pixel position. For the End key the composable requests extra range (endExtraX / endExtraY options) so the scroll clamp extends past the virtual content (the loading slot below the items).
  • getLoadingSlotSize (optional): Height of the loading slot. When provided, End includes it in the target so the last item plus the slot fit in the viewport.
  • activationMode (optional, accepts a ref or getter): 'viewport' (default) scrolls the viewport only and tracks no active item; 'item' moves a roving active item and exposes it through activeIndex/isActive/aria-activedescendant. The component derives it from the container role - see keyboardActivation.
  • onActivate (optional): Called with the item index when the active item is activated - Enter/Space on the container, or an explicit handleItemActivate call. Never called in 'viewport' mode.
  • The engine handles it drives - props, scrollDetails, scrollToIndex, stopProgrammaticScroll and the index/offset resolvers (getRowHeight, getRowOffset, getItemOffset, getItemSize, getRowIndexAt, getColumnIndexAt, etc.) - are the ones documented under useVirtualScroll.

Activation Models

'viewport' mode (the default) never tracks an item:

  • Home/End scroll to the start or end of the content. End targets totalSize - viewportSize (plus the loading slot size when getLoadingSlotSize is provided), the target is re-clamped when measurements settle, and new content appended by a load is not chased automatically. Because the slot lives in the DOM after the virtual wrapper, the requested range also extends the engine's scroll clamp, so the slot is actually reachable.
  • PageUp/PageDown scroll by one full page: the target is the first visible item minus one (startIdx - 1) or the last visible item plus one (endIdx + 1), so each press advances exactly one viewport.
  • Arrows move one item in the scroll direction (one column in grid mode). Enter/Space do nothing.

'item' mode moves a roving active item, announced through liveMessage:

  • Arrows move the active item by one along the scroll axis (ArrowLeft/ArrowRight on horizontal lists, honouring RTL; in grid mode the active item is a row, so the inline arrows keep panning columns) and scroll it back into view only when it left the viewport. The first arrow press activates the first visible item without scrolling.
  • PageUp/PageDown/Home/End move the active item one viewport up/down, or to the first/last item, and keep it visible. The other axis is left untouched, so a multi-column list keeps its current column.
  • Enter/Space activate the active item through onActivate.

Return Value

MemberTypeDescription
handleKeyDownFunctionKeyboard event handler to be bound to the focusable scroll container.
activeIndexRef<number>Ref with the roving active item index, -1 when none.
liveMessageRef<string>Polite announcement for the active item, e.g. Item 21 of 100; empty when nothing is active.
setActiveIndex(index: number | null) => voidSets (null clears) the active item without scrolling, so a click or an external selection can be synced in.
handleItemActivate(index: number) => voidMarks an item active and calls onActivate (e.g. from a click handler in the item slot). No-op in 'viewport' mode.

useVirtualScrollObservers

Creates and manages the ResizeObservers behind dynamic item and container sizes.

import { useVirtualScrollObservers } from '@pdanpdan/virtual-scroll';

const { setItemRef } = useVirtualScrollObservers({
  hostRef,
  wrapperRef,
  headerRef,
  footerRef,
  itemRefs,
  updateHostOffset: () => { /* ... */ },
  updateItemSizes: (updates) => { /* ... */ },
  // ...
});

Parameters

Accepts an UseVirtualScrollObserversOptions object.

Return Value

MemberTypeDescription
setItemRefFunctionCallback ref to be used on rendered items to track and measure them.

Extension Reference

VirtualScrollExtension

An extension is a plain object: a unique name plus the optional lifecycle hooks below. Pass them as the second argument of useVirtualScroll(props, extensions); the components wire the built-ins in this order: rtl, snapping, sticky, infinite-loading, prepend-restoration, coordinate-scaling.

interface VirtualScrollExtension<T> {
  name: string;
  onInit?(ctx: ExtensionContext<T>): void;
  onScroll?(ctx: ExtensionContext<T>, event: Event): void;
  onScrollEnd?(ctx: ExtensionContext<T>): void;
  // Post-processes the rendered window; must return the items to render.
  transformRenderedItems?(items: RenderedItem<T>[], ctx: ExtensionContext<T>): RenderedItem<T>[];
}

Every hook receives an ExtensionContext<T>: the reactive props, scrollDetails, totalSize, range and currentIndex; the engine state refs in internalState (scrollX/scrollY, internalScrollX/internalScrollY, isRtl, isScrolling, isProgrammaticScroll, viewport size, scaleX/scaleY, scroll directions, relative scroll, and isHydrated); and the engine methods in methods (scrollToIndex, scrollToOffset, updateDirection, getRowIndexAt, getColumnIndexAt, getItemSize, getItemBaseSize, getItemOffset, getColumnWidth, getColumnOffset, getItemRawOffset, handleScrollCorrection). On a grid the axis-specific resolvers differ: getItemSize/getItemOffset describe rows and getColumnWidth/getColumnOffset describe columns.

useRtlExtension

Detects the text direction of the scroll container (LTR or RTL) and flips horizontal offsets so items land in the right place.

import { useRtlExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useRtlExtension(),
]);

Parameters

This extension does not accept any parameters.

Behavior

  • Asks the engine to resolve the container direction while the extensions initialize.
  • The engine owns the detection: it reads the container (or the document root when the container is the window) on mount, on resize, on scroll and on dir/style changes.
  • Horizontal item offsets are flipped while the container is in RTL mode.

useSnappingExtension

Aligns the viewport to an item when scrolling stops, following the snap prop.

import { useSnappingExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useSnappingExtension(),
]);

Parameters

This extension does not accept any parameters.

Behavior

  • Hooks into onScrollEnd lifecycle event.
  • Resolves the snap target with the resolveSnap utility.
  • Uses scrollToIndex with behavior: 'smooth' to perform the snap.
  • Automatically ignores items larger than the viewport to prevent infinite jumping.

useStickyExtension

Sticky rows and columns, driven by the stickyIndices, stickyHeader and stickyFooter props. The extension owns the pinning: it keeps the nearest sticky item above the window rendered through includeIndices and computes isStickyActive/stickyOffset for the items in view. Without it, stickyIndices still reserves the layout offsets but nothing pins.

import { useStickyExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useStickyExtension(),
]);

Parameters

This extension does not accept any parameters.

Behavior

  • Sticky items stick below the sticky header (and above the sticky footer): activation and the pushing effect are measured from the sticky start/end offsets (stickyStartX/stickyStartY on StickyParams).
  • Supports both horizontal and vertical stickiness.

useInfiniteLoadingExtension

Calls back once the scroll position comes within the configured distance of the end, so you can append the next page of items.

import { useInfiniteLoadingExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useInfiniteLoadingExtension({
    onLoad: (axis, { velocity, direction }) => {
      // velocity: VU per millisecond along the axis; direction: 'start' | 'end' | null
      void loadNextPage(axis, direction);
    },
    flingVelocity: 2, // skip while the axis is still flinging faster than this
    preload: 0, // VU added to the threshold while scrolling towards the end
  }),
]);

Parameters

PropertyTypeDescription
onLoad(axis: 'vertical' | 'horizontal', details: LoadDetails) => voidCallback triggered when a threshold is met. details.velocity is the scroll velocity along the axis in VU per millisecond and details.direction the travel direction ('start' | 'end' | null).
flingVelocity Default 2numberWhile the axis moves faster than this (VU/ms) the callback is skipped and fires once the scroll slows below it, so a fling cannot load several pages in one gesture.
preload Default 0numberExtra distance in VU added to the loadDistance threshold, but only while the travel direction points at the end being checked - scrolling back towards the start does not load the next page earlier.

Behavior

  • Watches scrollDetails reactively and reports the velocity and travel direction it observed to onLoad.
  • Respects the loadDistance and loading props from the component.
  • Prevents duplicate triggers while loading is true.
  • Skips the callback while the axis is flinging faster than flingVelocity (default 2 VU/ms) and fires once it slows down.
  • Extends the threshold by preload (VU, default 0) only while the travel direction points at the end being checked.
  • Fires as soon as the threshold is reached - including while a programmatic scroll (scrollbar drag, PageDown/End) is still settling - so the loading indicator appears promptly and is not skipped.

usePrependRestorationExtension

Keeps the visible position when items are prepended to the front of items - what chat and log views need, so the reader stays where they were while older entries load above.

import { usePrependRestorationExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  usePrependRestorationExtension(),
]);

Parameters

This extension does not accept any parameters.

Behavior

  • Compares new items with previous items to detect prepended count.
  • Calculates the height (or width) of prepended items.
  • Uses handleScrollCorrection to silently adjust the scroll position before the next frame.

useSnapshotsExtension

Keeps the visible position across a save/restore cycle. save() captures it, restore() brings it back - immediately, or on a later visit when a persistent storage is configured - and clear() drops both the in-memory snapshot and the stored entry. A snapshot only records the first visible index, the offset inside that item (VU) and the item count it was taken with, so it stays small and portable.

import { useSnapshotsExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const snapshots = useSnapshotsExtension({
  storage: 'session', // 'memory' (default) | 'session' | 'local' | a Storage object
  key: 'virtual-scroll:snapshot', // storage key for the persistent modes
  autoSave: false, // save on every scroll end
});

const vs = useVirtualScroll(props, [
  snapshots,
]);

// When the list is left / unmounted:
snapshots.save();

// On a later visit, once the same items are in place again:
snapshots.restore();

// Forget the position for good:
snapshots.clear();

Parameters

PropertyTypeDescription
storage Default 'memory''memory' | 'session' | 'local' | StorageWhere snapshots are persisted. 'memory' keeps them inside the extension for the lifetime of the page; 'session' / 'local' use sessionStorage / localStorage; any other value must implement the Storage interface. A storage that is unavailable (SSR, privacy mode, quota exceeded) falls back to memory without throwing.
key Default 'virtual-scroll:snapshot'stringStorage key used by 'session', 'local' and a custom Storage. Ignored in 'memory' mode.
autoSave Default falsebooleanSave on every scroll end (keeping the snapshot in memory too), so you do not have to call save() yourself.

Return Value

On top of the extension contract (its name is 'snapshots') the extension adds:

MemberTypeDescription
save() => ScrollSnapshotCapture the current scroll position, keep it in memory and persist it when a storage is configured; returns the captured snapshot.
restore(snapshot?: ScrollSnapshot | null) => booleanScroll back to the given snapshot, or to the last saved one when omitted. Returns false without scrolling when the snapshot is missing or invalid, or when its total no longer matches the item count.
clear() => voidDrop the in-memory snapshot and the stored entry.

Behavior

  • Scrolls back through the engine's scroll API, so the restored position is expressed in the same first-visible-index plus in-item offset terms that were captured.
  • Refuses a snapshot whose total no longer matches the item count: a list that changed shape falls back to its normal start position instead of jumping to an unrelated index.
  • Ignores anything that is not a usable snapshot (missing or non-numeric index/offset/total), including a corrupted stored entry.
  • Restore after the items are back: the count check runs against the current list, so a snapshot taken with 100 items is refused while the list is still loading (or has grown or shrunk) and accepted once it holds 100 items again.
  • A blocked or full storage is not fatal - the extension keeps working from memory.

useCoordinateScalingExtension

Lets a list scroll far past the browser's element size limit (roughly 10M to 30M px) by scaling the display coordinates, so virtual content can span billions of pixels.

import { useCoordinateScalingExtension, useVirtualScroll } from '@pdanpdan/virtual-scroll';

const vs = useVirtualScroll(props, [
  useCoordinateScalingExtension(),
]);

Parameters

This extension does not accept any parameters.

Behavior

  • Calculates scaleX and scaleY factors when total size exceeds browser limits.
  • Transparently maps physical scroll events to virtual positions.
  • Automatically disabled when using window as the container (as the browser handles body scrolling differently).

API Reference

Types

ScrollDirection

'vertical' | 'horizontal' | 'both'

Defines the virtualization axes for the VirtualScroll component.

ScrollAxis

'vertical' | 'horizontal'

Used specifically for individual scrollbar instances.

ScrollDetails<T>

PropertyTypeDescription
itemsRenderedItem<T>[]Rendered items in the buffer.
currentIndexnumberFirst visible row index below any sticky header.
currentColIndexnumberFirst visible column index after any sticky column.
currentEndIndexnumberIndex of the last item visible above any sticky footer.
currentEndColIndexnumberIndex of the last column visible before any sticky end column (grid mode).
scrollOffset{ x, y }Current relative scroll position in virtual units (VU).
displayScrollOffset{ x, y }Current physical scroll position in display pixels (DU).
viewportSize{ width, height }Dimensions of the visible viewport in virtual units (VU).
displayViewportSize{ width, height }Physical dimensions of the visible viewport in display pixels (DU).
totalSize{ width, height }Estimated total content dimensions (VU).
isScrollingbooleanActive scrolling state.
isProgrammaticScrollbooleanTrue if triggered by scrollToIndex/Offset.
range{ start, end }Range of currently rendered item indices, including the scroll buffer (inclusive start, exclusive end).
columnRangeColumnRangeVisible column range (grid).

RenderedItem<T>

PropertyTypeDescription
itemTThe source data item.
indexnumberItem's position in the array.
offset{ x, y }Absolute pixel position within the wrapper (DU).
size{ width, height }Current dimensions (VU).
originalX / originalYnumberOffsets before any sticky adjustments (VU).
isStickybooleanIs configured as sticky.
isStickyActivebooleanCurrently stuck to the edge.
isStickyActiveX / isStickyActiveYbooleanCurrently stuck to the horizontal/vertical edge respectively.
stickyOffset{ x, y }Translation applied for sticky pushing effect (DU).

ColumnRange

PropertyTypeDescription
startnumberIndex of first rendered column.
endnumberIndex of last rendered column (exclusive).
padStartnumberPixel space to maintain before columns (VU).
padEndnumberPixel space to maintain after columns (VU).

LoadDetails

Second argument of the load event and of the infinite loading onLoad callback.

PropertyTypeDescription
velocitynumberScroll velocity on the axis in virtual units (VU) per millisecond; 0 when it cannot be measured.
direction'start' | 'end' | nullDirection of the scroll on the axis, or null when no direction has been observed yet.

ScrollSnapshot

A saved position, returned by save() and accepted by restore() on the snapshots extension.

PropertyTypeDescription
indexnumberIndex of the first visible item at the saved position.
offsetnumberOffset (VU) inside that item, measured from its start.
totalnumberNumber of items the snapshot was taken with; restore() refuses it when the current count differs.

VirtualScrollProps<T>

Core configuration properties shared between the component and the composables (a subset of the full prop tables above; hostElement is accepted by the composable only).

PropertyTypeDescription
itemsT[]Data source. Required.
itemSizenum | arr | fn | nullSizing logic (fixed, circular array pattern, or function). Omitted/null measures rows; the estimate before measurement is 40px.
directionScrollDirection'vertical' | 'horizontal' | 'both'.
bufferBefore / bufferAfternumberItems outside viewport. Default: 5.
containerHTMLElement | WindowScroll container. Defaults to component root.
hostElementHTMLElementReference for offset calculation (DU).
ssrRangeSSRRangePre-rendered range for SSR.
columnCountnumberTotal columns for grid mode.
columnWidthnum | arr | fn | nullColumn sizing. Omitted/null measures columns; the estimate before measurement is 100px.
scrollPaddingStart / Endnum | {x, y}Pixel offsets for scroll limits.
gap / columnGapnumberPixel space between items/cols.
restoreScrollOnPrependbooleanMaintain chat scroll position.
keyboardActivation'auto' | 'item' | 'viewport'Keyboard model. 'auto' (default) tracks a roving active item only for roles with an active descendant (listbox, menu, tree), and scrolls the viewport for every other role.
snapSnapModeAuto-alignment after scroll stop.
initialScrollIndexnumberMount-time jump index.
initialScrollAlignScrollAlignment | OptionsAlignment for initial jump.
defaultItemSizenumberEstimate for dynamic items.
defaultColumnWidthnumberEstimate for dynamic columns.
debugbooleanEnable visualization.

StickyParams

Parameters for calculating sticky item offsets (core engine's calculateStickyItem). Part of the engine layer: import it from @pdanpdan/virtual-scroll/internal; no compatibility guarantee.

PropertyTypeDescription
indexnumberItem index.
isStickybooleanWhether the item is configured as sticky.
directionScrollDirectionScroll direction.
relativeScrollX / relativeScrollYnumberVirtual scroll position (VU).
originalX / originalYnumberVirtual original position of the item (VU).
width / heightnumberVirtual item size (VU).
stickyIndicesnumber[]All configured sticky indices.
fixedSize / fixedWidthnumber | nullFixed item size / column width (VU), null for dynamic.
gap / columnGapnumberItem / column gap (VU).
getItemQueryY / getItemQueryX(index: number) => numberPrefix sum resolvers for offsets (VU).
stickyStartX / stickyStartYnumberSize of sticky start elements (left/top) in DU. Sticky items stick below them; activation and the pushing effect are measured from this offset. Optional, defaults to 0.

UseVirtualScrollbarProps

PropertyTypeDescription
axisScrollAxisAxis of the scrollbar.
totalSizenumberTotal size of content in pixels.
positionnumberCurrent scroll position in pixels.
viewportSizenumberVisible area size in pixels.
scrollToOffset(offset: number) => voidCallback to update position.
containerIdstringID for accessibility.
isRtlbooleanEnable RTL mapping.
ariaLabelstringAccessible label for the scrollbar.

ScrollToIndexOptions

Full configuration for index-based scrolling.

PropertyTypeDescription
alignScrollAlignment | OptionsWhere to align the item (default: 'auto').
behavior'auto' | 'smooth'Scroll behavior (default: 'smooth').

ScrollAlignmentOptions

Allows axis-specific alignment in scrollToIndex.

PropertyTypeDescription
xScrollAlignmentAlignment on the horizontal axis.
yScrollAlignmentAlignment on the vertical axis.

ScrollAlignment

Controls the item's final position in the viewport during scrollToIndex.

ValueBehavior
'start'Aligns to top (vertical) or left (horizontal) edge.
'center'Aligns to viewport center.
'end'Aligns to bottom (vertical) or right (horizontal) edge.
'auto' DefaultIf the item is already fully visible, no scroll occurs. Otherwise, aligns to 'start' or 'end' to bring it into view.

SnapMode

Defines how items align when user scrolling stops. Snapping is disabled for items larger than the viewport.

ValueBehavior
falseNo snapping (default).
true / 'auto'Direction-aware: if scrolling towards start, acts as 'end'. If scrolling towards end, acts as 'start'.
'next'Snaps to the next (closest) snap position in the direction of the scroll.
'start'Snaps the first visible item to the top/left edge if >= 50% is visible, otherwise snaps the next item.
'center'Snaps the item intersecting the viewport center to be exactly centered.
'end'Snaps the last visible item to the bottom/right edge if >= 50% is visible, otherwise snaps the previous item.

UseVirtualScrollSizesProps

Config of the internal sizing composable - import it from @pdanpdan/virtual-scroll/internal; no compatibility guarantee.

PropertyTypeDescription
propsVirtualScrollProps or a ref/getterVirtual scroll configuration; pass a ref or getter when it is derived, so bounds, axis and column count stay live.
isDynamicItemSizebooleanWhether items have dynamic heights/widths.
isDynamicColumnWidthbooleanWhether columns have dynamic widths.
defaultSizenumberFallback size for items before they are measured.
fixedItemSizenumber | nullFixed item size if applicable.
directionScrollDirectionThe scroll direction.

FenwickTree

A Fenwick tree (binary indexed tree): O(log n) prefix sums and point updates.

MethodSignatureDescription
update(index, delta) => voidUpdate value at index and propagate changes.
query(index) => numberGet prefix sum up to index (exclusive).
get(index) => numberGet individual value at index.
set(index, value) => voidSet the individual value at an index without updating the prefix sum tree.
getValues() => Readonly<Float64Array>Get the underlying values as a read-only view (logical size).
lengthnumberLogical number of items in the tree (read-only property).
findLowerBound(value) => numberFind largest index where prefix sum <= value.
rebuild() => voidRebuild tree from current values in O(n).
resize(size) => voidResize tree while preserving values.
shift(offset) => voidShift values by offset (useful for prepending).

Methods

Detailed reference for the methods exposed on the VirtualScroll component instance (via a template ref) and for the helpers returned by the composables - the badge names the owning API. Methods on the instance are also returned by useVirtualScroll; helpers badged useVirtualScrollSizes belong to the internal sizing layer (@pdanpdan/virtual-scroll/internal, no compatibility guarantee).

Method scrollToIndex()

scrollToIndex(
rowIndex?: number | null,
colIndex?: number | null,
options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions
): ScrollToIndexResult

Scrolls a specific item into view. If the item's size is dynamic and not yet measured, the scroll position is corrected after rendering. Returns the computed scroll targets in virtual and display units (ScrollToIndexResult).

ParameterTypeDescription
rowIndexnumber | nullTarget row. null to keep current Y. Optional.
colIndexnumber | nullTarget column. null to keep current X. Optional.
optionsOptionsAlignment and behavior settings.

Method scrollToOffset()

scrollToOffset(
x?: number | null,
y?: number | null,
options?: ScrollToOffsetOptions // { behavior?: 'auto' | 'smooth', endExtraX?: number, endExtraY?: number }, behavior default: 'auto'
): void

Scrolls the container to an absolute pixel position. Clamped between 0 and the calculated total size; the target is re-clamped when measurements settle (dynamic items).

endExtraX / endExtraY append extra scrollable range (VU) after the content end on that axis, so a block rendered below the items - e.g. an always-rendered loading slot - stays reachable.

Method refresh()

Invalidates all cached measurements and triggers a full re-initialization. Use this if your item source data changes in a way that affects sizes without changing the items array reference.

Method updateItemSize()

updateItemSize(
index: number,
inlineSize: number,
blockSize: number,
element?: HTMLElement
): void

Manually registers a new measurement for a single item. The element parameter allows the virtualizer to detect columns from any internal structure using data-col-index attributes.

Method updateItemSizes()

updateItemSizes(updates: Array<{ index: number; inlineSize: number; blockSize: number; element?: HTMLElement }>): void

Batched version of updateItemSize. More efficient when many items are measured simultaneously.

Method updateHostOffset()

Forces a recalculation of the host element's position relative to the scroll container. Call this if the layout changes in a way that shifts the component without triggering a resize event.

Method updateDirection()

Manually triggers the detection of the scroll direction (LTR or RTL). The component also performs this automatically on mount and whenever the container prop changes.

Method getColumnWidth()

getColumnWidth(index: number): number

Returns the currently calculated width for a specific column index, taking measurements and gaps into account.

Method getRowHeight()

getRowHeight(index: number): number

Returns the currently calculated height for a specific row index, taking measurements and gaps into account.

Method getRowOffset()

getRowOffset(index: number): number

Returns the virtual vertical offset (top) of a row in virtual units (VU).

Method getColumnOffset()

getColumnOffset(index: number): number

Returns the virtual horizontal offset (left) of a column in virtual units (VU).

Method getItemOffset()

getItemOffset(index: number): number

Returns the virtual offset of an item along the scroll axis in virtual units (VU).

Method getItemSize()

getItemSize(index: number): number

Returns the size of an item along the scroll axis in virtual units (VU).

Method getRowIndexAt()

getRowIndexAt(offset: number): number

Returns the row (or item) index at a specific vertical (or horizontal in horizontal mode) virtual offset (VU).

Method getColumnIndexAt()

getColumnIndexAt(offset: number): number

Returns the column index at a specific horizontal virtual offset (VU).

Method getItemAriaProps()

getItemAriaProps(index: number): Record<string, string | number | undefined>

Returns the ARIA attributes for an item at the given index. Includes role, aria-setsize, and aria-posinset (or aria-rowindex for grids).

Method getCellAriaProps()

getCellAriaProps(colIndex: number): Record<string, string | number | undefined>

Returns the ARIA attributes for a cell at the given column index. Only relevant for direction="both" or role="grid". Includes role="gridcell" and aria-colindex.

Method stopProgrammaticScroll()

Immediately halts any active smooth scroll animation and clears pending scroll requests.

Method setActiveIndex()

setActiveIndex(index: number | null): void

Sets the item keyboard navigation tracks as active without scrolling it into view, so a click or an external selection can be synced in. Pass null (or a negative index) to clear the selection; an index past the end clamps to the last item. The active item is exposed through activeIndex and the isActive item slot prop.

Method handleItemActivate()

handleItemActivate(index: number): void

Marks an item as the active one and emits itemActivate with the index and the item - wire it to a click handler in the item slot. No-op in the viewport activation model, where nothing is ever activated (see Keyboard Navigation).

useVirtualScroll handleScrollCorrection()

handleScrollCorrection(addedX: number, addedY: number): void

Applies the delta accumulated by measurement changes above the viewport, keeping the visible content stable when item sizes settle.

useVirtualScrollSizes getItemBaseSize()

getItemBaseSize(item: T, index: number): number

Returns the configured base size for an item (itemSize function result or the default size) used before measurement.

useVirtualScrollSizes getSizeAt()

getSizeAt(index: number, sizeProp, defaultSize: number, gap: number, tree: FenwickTree, isX: boolean): number

Queries the size of an index from a Fenwick tree, honoring the configured size source, defaults, gaps and tree updates.

useVirtualScrollSizes initializeSizes()

initializeSizes(): void

Rebuilds the size trees from the configured sizes and clears all measurement flags.

useVirtualScrollInertia handlePointerDown()

handlePointerDown(event: PointerEvent): void

Starts scaled drag/inertia handling on pointer down.

useVirtualScrollInertia handlePointerMove()

handlePointerMove(event: PointerEvent): void

Tracks pointer movement while dragging (used by scaled touch/wheel inertia).

useVirtualScrollInertia handlePointerUp()

handlePointerUp(event: PointerEvent): void

Ends a drag sequence and launches inertia when needed.

useVirtualScrollInertia handleWheel()

handleWheel(event: WheelEvent): void

Handles wheel input when coordinate scaling is active so 1:1 movement is preserved.

useVirtualScrollInertia stopInertia()

stopInertia(): void

Immediately halts any running inertia animation.

useVirtualScrollKeyboard handleKeyDown()

handleKeyDown(event: KeyboardEvent): void

Implements keyboard navigation (arrows, Home/End, PageUp/PageDown) with alignment support.

useVirtualScrollObservers setItemRef()

setItemRef(el: unknown, index: number): void

Callback ref used by rendered items: registers/unregisters elements for dynamic measurement.

Utility Functions

The helpers below are the engine layer the components are built on. They are imported from @pdanpdan/virtual-scroll/internal - not from the package root - and carry no compatibility guarantee: shapes and signatures may change in any release, including a patch.

Type Guards

isElement(val?): Checks if a value is a standard HTMLElement (explicitly excluding window). Optional.

isWindow(val?): Checks for global window object. Optional.

isBody(val?): Checks for document.body. Optional.

isWindowLike(val?): Matches window or body. Optional.

isScrollableElement(val?): Checks if a value is an HTMLElement that exposes native scroll properties like scrollLeft. Optional.

isScrollToIndexOptions(val): Type guard for ScrollToIndexOptions object.

getPaddingX / getPaddingY

(p?: number | { x?: number; y?: number } | null, direction?: ScrollDirection): number

Resolves a scroll-padding value for the target axis: a number applies along the scroll axis, an object supplies per-axis x/y.

Coordinate Mapping

displayToVirtual(displayPos, hostOffset, scale): Maps display pixels (DU) to virtual content position (VU).

virtualToDisplay(virtualPos, hostOffset, scale): Maps virtual content position (VU) to display pixels (DU).

isItemVisible

(itemPos, itemSize, scrollPos, viewSize, stickyStart?, stickyEnd?): boolean

Highly accurate visibility check (VU) used for auto-alignment and rendering ranges.

Default Values & Constants

DEFAULT_ITEM_SIZE40px
DEFAULT_COLUMN_WIDTH100px
DEFAULT_BUFFER5 items
DEFAULT_MASONRY_TARGET_COLUMN_WIDTH240px
DEFAULT_MASONRY_MIN_COLUMNS1
DEFAULT_MASONRY_MAX_COLUMNS10
DEFAULT_MASONRY_GAP10px
DEFAULT_MASONRY_SEGMENT_SIZE500 items
BROWSER_MAX_SIZE10,000,000px
EMPTY_SCROLL_DETAILSzeroed ScrollDetails

Values applied when props are omitted or dynamic estimates are needed. BROWSER_MAX_SIZE defines the scaling threshold. EMPTY_SCROLL_DETAILS is a zeroed ScrollDetails object for placeholder state - spread it ({ ...EMPTY_SCROLL_DETAILS }) and override the fields you have.

SSR & Hydration

The library supports Server-Side Rendering via the ssrRange prop. When provided, the specified items are rendered "in-flow" on the server.

Hydration is automatic: the client renders the same in-flow items before mounting to match the server HTML, then scrolls to the pre-rendered range and switches to absolute positioning for virtualization.