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:
- Enable them with the
virtualScrollbarprop onVirtualScroll(they are also enabled automatically for lists beyond the browser size limit). - Match your design using the --vs-scrollbar-* CSS variables, or take full control of the scrollbar UI with the
scrollbarscoped 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/columnWidthgives O(1) range math; arrays and functions use O(log n) Fenwick-tree lookups; dynamic (measured) sizes are the most expensive and re-measure withResizeObserver. See the Sizing Guide below. - Skip per-row data for uniform lists. A numeric
itemSize/columnWidthallocates no per-row storage and positions rows arithmetically, so index-only lists (sparseitems, e.g.new Array(10_000_000)) keep memory flat at any scale - render row content from the slot'sindexinstead ofitem. - 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
ssrRangeto 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.
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.
| Type | itemSize / columnWidth | Perf | Description |
|---|---|---|---|
| Fixed | number | Best | Uniform size for all items. Calculations are O(1). |
| Array (Circular Pattern) | number[] | Great | Repeating size patterns from array (e.g. [50, 100]). O(log n). |
| Function | (item, idx) => number | Good | Known but variable sizes. No ResizeObserver overhead unless measured size differs. |
| Dynamic | 0, null, undefined | Fair | Sizes 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
renderedItemsyourself, 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.
CDN Stand-alone Examples
Stand-alone HTML pages that load Vue and the library from a CDN.
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
useRtlExtension(): RTL layout detection.useSnappingExtension(): Snapping to item edges.useStickyExtension(): Sticky headers, footers and indices.useInfiniteLoadingExtension(): Loads more data near the end.usePrependRestorationExtension(): Keeps the position when items are prepended.useSnapshotsExtension(): Saves and restores the visible position.useCoordinateScalingExtension(): Lists past the browser size limit.
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
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | - | 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. |
itemSize | num | arr | fn | null | 40 (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. |
gap | number | 0 | Spacing between items (vertical or horizontal). |
Grid Configuration (only for direction="both")
| Prop | Type | Default | Description |
|---|---|---|---|
columnCount | number | 0 | Number of columns for grid mode. |
columnWidth | num | arr | fn | null | 100 (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. |
columnGap | number | 0 | Spacing between columns. |
Features & Behavior
| Prop | Type | Default | Description |
|---|---|---|---|
stickyIndices | number[] | [] | Indices of items that should remain sticky. When stickyHeader or stickyFooter are enabled, they stick below or above them. |
stickyHeader / stickyFooter | boolean | false | If 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. |
loading | boolean | false | While true, reveals the #loading slot (the slot stays mounted when provided and is hidden via CSS while false) and suppresses repeated load events. |
loadDistance | number | 200 | Distance from the end to trigger the load event. |
snap | bool | SnapMode | false | Automatically align to nearest item after scroll stops. See SnapMode. |
virtualScrollbar | boolean | false | Whether to force the use of virtual scrollbars. Enabled automatically for massive lists, and disabled when window or body is the container. |
restoreScrollOnPrepend | boolean | false | Maintain scroll position when items are added to the top. |
container | HTMLElement | Window | hostRef | The scrollable container. Defaults to the component's root element. Pass window or document.body to scroll the page. |
initialScrollIndex | number | - | Index to jump to on mount. |
initialScrollAlign | ScrollAlignment | Options | 'start' | Alignment for initial index. |
Accessibility
| Prop | Type | Default | Description |
|---|---|---|---|
role | string | '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. |
ariaLabel | string | - | Accessible label for the scroll container. |
ariaLabelledby | string | - | ID of the element that labels the scroll container. |
itemRole | string | - | 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
| Prop | Type | Default | Description |
|---|---|---|---|
containerTag | string | 'div' | HTML tag for the scroll container, e.g. for semantic list markup. For tabular data use the VirtualScrollTable component. |
wrapperTag | string | 'div' | HTML tag for the items wrapper. Combine 'ul'/'ol' with itemTag: 'li' for semantic lists. Tables should use the VirtualScrollTable component. |
itemTag | string | 'div' | HTML tag for each virtualized item (e.g. 'li'). For table rows use VirtualScrollTable. |
headerTag | string | 'div' | HTML tag for the header slot wrapper (e.g. 'header'). Tables use VirtualScrollTable, whose header slot renders the <thead>. |
footerTag | string | 'div' | HTML tag for the footer slot wrapper (e.g. 'footer'). Tables use VirtualScrollTable, whose footer slot renders the <tfoot>. |
scrollPaddingStart / End | num | {x, y} | 0 | Additional padding for scroll offsets. |
bufferBefore / bufferAfter | number | 5 | Number of items to render outside the viewport. |
defaultItemSize | number | 40 | Estimated size for items before measurement. |
defaultColumnWidth | number | 100 | Estimated width for columns before measurement. |
debug | boolean | false | Enables debug mode (visible offsets and indices). |
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 Prop | Default Item Role | Behavior |
|---|---|---|
list (default) | listitem | Standard 1D list. |
grid | row | 2D data grid or table. |
tree | treeitem | Hierarchical structure. |
listbox | option | Selectable list. |
menu | menuitem | Navigation menu. |
Slots
#item
Scoped slot for individual items.
item: T: The data item from the source array (undefinedfor holes in sparse or index-only datasets).index: number: The original 0-based index of the item.isSticky: boolean:trueif the item is configured to be sticky viastickyIndices.isStickyActive: boolean:trueif the item is currently stuck at the threshold.isStickyActiveX / Y: boolean:trueif the item is stuck at the horizontal or vertical threshold.isActive: boolean:truefor 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 - orsetActiveIndex/handleItemActivateselect 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 forVirtualScrollbar.axis: 'vertical' | 'horizontal'totalSize: numberposition: numberviewportSize: numberscrollToOffset: (offset: number) => voidcontainerId: stringisRtl: booleanariaLabel: 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 }">
<!-- 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>
<!-- 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>| Property | Type | Description |
|---|---|---|
axis | 'vertical' | 'horizontal' | The scrollbar axis. |
positionPercent | number | Scroll position percentage (0-1). |
viewportPercent | number | Viewport percentage of total (0-1). |
thumbSizePercent | number | Calculated thumb size percentage (0-100). |
thumbPositionPercent | number | Calculated thumb position percentage (0-100). |
trackProps | Record<string, unknown> | Attributes/listeners for the track. Bind with v-bind="trackProps". Includes class and style. |
thumbProps | Record<string, unknown> | Attributes/listeners for the thumb. Bind with v-bind="thumbProps". Includes class and style. |
scrollbarProps | VirtualScrollbarProps | Grouped props for the VirtualScrollbar component: axis, totalSize, position, viewportSize, scrollToOffset, containerId, isRtl, ariaLabel. Useful for <VirtualScrollbar v-bind="scrollbarProps" />. |
isDragging | boolean | Whether the thumb is currently being dragged. |
Events
| Event | Payload | Description |
|---|---|---|
scroll | ScrollDetails<T> | Emitted on every scroll position change. |
load | 'vertical' | 'horizontal', LoadDetails | Emitted 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). |
itemActivate | index: number, item: T | undefined | Emitted 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'):
#loading slot is present.snap mode).snap mode).Roving active item ('item'):
CSS Classes
| Class | Description |
|---|---|
.virtual-scroll-container | The root scrollable container element. |
.virtual-scroll-wrapper | Wraps rendered items and provides total scrollable dimensions. |
.virtual-scroll-item | Applied to each individual rendered item. Use for general item styling. |
.virtual-scroll-header / .virtual-scroll-footer | Containers for header and footer slots. |
.virtual-scroll-loading | Container for the loading slot. |
.virtual-scroll-loading--hidden | Applied 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 / --both | Direction modifiers applied to the root container. |
.virtual-scroll--hydrated | Applied after client-side mount and hydration is complete. |
.virtual-scroll--window | Applied when scrolling via the global window object. |
.virtual-scroll--table | Applied by the VirtualScrollTable component. |
.virtual-scroll--sticky | Applied to items that are currently stuck to the viewport edge. |
.virtual-scroll--debug | Visible when debug prop is active. |
.virtual-scroll--hide-scrollbar | Applied when virtual scrollbars are enabled or content is massive. |
CSS Variables
The default VirtualScrollbar can be styled using the following CSS variables:
| Variable | Default (Light/Dark) | Description |
|---|---|---|
--vs-scrollbar-bg | rgba(230,230,230,0.9) / rgba(30,30,30,0.9) | Track background color. |
--vs-scrollbar-thumb-bg | rgba(0,0,0,0.3) / rgba(255,255,255,0.3) | Thumb background color. |
--vs-scrollbar-thumb-hover-bg | rgba(0,0,0,0.6) / rgba(255,255,255,0.6) | Thumb background on hover/active. |
--vs-scrollbar-size | 8px | Width (vertical) or height (horizontal) of the scrollbar. |
--vs-scrollbar-radius | 4px | Border radius for track and thumb. |
--vs-scrollbar-cross-gap | var(--vs-scrollbar-size) | Size of gap to use where scrollbars meet. |
--vs-scrollbar-has-cross-gap | 0 | If 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
All PropsAll component props are available on the instance.
scrollDetailsFull reactive state of the virtualizer.
activeIndexIndex of the item tracked by keyboard navigation, -1 when none.
columnRangeVisible column indices and paddings.
isHydratedMounted and ready for virtualization.
isRtlRight-to-Left mode active.
scrollbarPropsVerticalReactive vertical scrollbar properties.
scrollbarPropsHorizontalReactive horizontal scrollbar properties.
scaleX / scaleYCurrent coordinate scaling factors.
componentOffsetAbsolute offset of the component within its container.
renderedWidth / renderedHeightPhysical dimensions in DOM (clamped).
wrapperRole / cellRoleThe ARIA roles currently applied to the wrapper and its cells.
Methods
scrollToIndex()Scroll to a specific row/column.
scrollToOffset()Scroll to precise pixel position.
stopProgrammaticScroll()Halt smooth scroll animations.
setActiveIndex()Set (null clears) the active item without scrolling.
handleItemActivate()Mark an item active and emit itemActivate.
getColumnWidth()Get calculated width of a column.
getRowHeight()Get calculated height of a row.
getRowOffset()Get virtual offset of a row.
getColumnOffset()Get virtual offset of a column.
getItemOffset()Get virtual offset of an item.
getItemSize()Get item size along scroll axis.
getRowIndexAt()Get row index at virtual offset.
getColumnIndexAt()Get column index at virtual offset.
getItemAriaProps()Get ARIA attributes for an item.
getCellAriaProps()Get ARIA attributes for a cell.
refresh()Reset all dynamic measurements.
updateDirection()Trigger RTL/LTR detection.
updateHostOffset()Recalculate container position.
updateItemSize()Manually register measurement.
updateItemSizes()Batch register measurements.
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
| Prop | Type | Default | Description |
|---|---|---|---|
flowTable | boolean | false | Render 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. |
autoSizeColumns | boolean | false | Pin 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. |
columnWidths | number[] | - | Explicit column widths (px) pinned via the colgroup; takes precedence over autoSizeColumns. |
stickyHeader / stickyFooter | boolean | false | Measure and reserve the header/footer slots; the row groups stick to the viewport edges. |
virtualScrollbar | boolean | false | Force 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,loadingandscrollbarslots. Theitemslot 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,loadanditemActivateevents (a table tracks an active row only withkeyboardActivation: 'item'). See Events. - Exposed instance (via ref): the same methods and state -
scrollToIndex, scrollToOffset,refresh,updateItemSizes,stopProgrammaticScroll,scrollDetails,isHydrated,getItemOffset/getItemSizeand 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 isVirtualScrollTableInstance<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 andrtl. Tag customization (containerTag/wrapperTag/itemTag) lives onVirtualScrollfor semantic lists (e.g.ul/ol > li). OnVirtualScrollTablethe 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), anddirectionis 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | Required | Array 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. |
itemHeight | fn(item, index, columnWidth) | Required | Canonical 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. |
targetColumnWidth | number | 240 | Desired 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 / maxColumns | number | 1 / 10 | Column count bounds for the responsive reflow. |
measuredHeights | boolean | false | Measure 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. |
gap | number | 10 | Spacing between cards in px, applied both between columns and between rows of the layout. |
segmentSize | number | 500 | Items 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. |
virtualScrollbar | boolean | true | Render the overlay virtual scrollbar (the native one is hidden while enabled). Hidden automatically when the content fits the viewport. |
role / itemRole | string | '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 / ariaLabelledby | string | - | Accessible label for the scroll container (the container role becomes region when labelled). |
debug | boolean | false | Outline rendered card bounds and overlay a geometry badge (#index (x, y)) per card. |
Item Slot
| Prop | Type | Description |
|---|---|---|
item / index | T | undefined / number | The original data item and its 0-based dataset index (undefined for sparse holes). |
column | number | The 0-based column the card was placed into. |
x / y | number | Card offset in px relative to the cards wrapper (the component positions the card itself via translate). |
width / height | number | Card 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>.
| Member | Type | Description |
|---|---|---|
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. |
scrollDetails | MasonryScrollDetails<T> | Current scroll state (same shape as the scroll event payload). |
columns / columnWidth | number | Live resolved column count and column width in px (0 until the container is measured) - e.g. for srcset candidates or text budgets. |
totalHeight / totalHeightExact | number / boolean | Content 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. |
scrollToIndex | fn(index?, options?) | Scroll to a card with align ('start' | 'center' | 'end' | 'auto'), behavior and dryRun; far jumps land on the exact canonical position. |
scrollToOffset | fn(offset?, options?) | Scroll to a pixel offset (use ±Infinity for the end/start; end intents follow the content as totals settle). |
refresh | fn() | 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. WithmeasuredHeights, cards size to their content and only mounted cards are measured (unmounted regions keep the oracle estimate). - Vertical axis only: no RTL, horizontal, or
bothmode 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">
<!-- Vertical Scrollbar -->
<VirtualScrollbar
axis="vertical"
:total-size="10000"
:viewport-size="400"
:position="scrollY"
@scroll-to-offset="val => scrollY = val"
/>
<!-- Horizontal Scrollbar -->
<VirtualScrollbar
axis="horizontal"
:total-size="10000"
:viewport-size="800"
:position="scrollX"
@scroll-to-offset="val => scrollX = val"
/>
</div>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
axis | 'vertical' | 'horizontal' | 'vertical' | The axis of the scrollbar. Defaults to 'vertical'. |
totalSize | number | - | Total size of the scrollable content in pixels. Required. |
viewportSize | number | - | Size of the visible viewport in pixels. Required. |
position | number | - | 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. |
containerId | string | undefined | ID of the container element for accessibility. |
isRtl | boolean | false | Whether the scrollbar is in Right-to-Left (RTL) mode. |
ariaLabel | string | - | Accessible label for the scrollbar. |
Events
| Event | Payload | Description |
|---|---|---|
scroll-to-offset | number | Emitted 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
| Member | Type | Description |
|---|---|---|
renderedItems | Ref<RenderedItem<T>[]> | List of items to render in the current buffer. |
scrollDetails | Ref<ScrollDetails<T>> | Full reactive state of the virtual scroll system. |
columnRange | Ref<ColumnRange> | Visible columns and their associated paddings. |
totalWidth / totalHeight | Ref<number> | Calculated total size of the scrollable content area (DU). |
renderedWidth / renderedHeight | Ref<number> | Total dimensions to be rendered in the DOM (clamped to browser limits, DU). |
isHydrated | Ref<boolean> | true when the component is mounted and hydrated. |
isRtl | Ref<boolean> | true if the scroll container is in Right-to-Left mode. |
scaleX / scaleY | Ref<number> | Current coordinate scaling factors (VU / DU). |
componentOffset | { x: Ref<number>, y: Ref<number> } | Absolute offset of the component in its container (DU). |
scrollbarOffset | Reactive<{ x: number; y: number }> | Inline-start/block-start padding of the scroll container (DU), used to align the virtual scrollbar overlay with the scrollport. |
activeIndex | Ref<number> | Index of the item tracked by keyboard navigation, -1 when none. |
| setActiveIndex | Function | Sets (null clears) the active item without scrolling, so a click or an external selection can be synced in. |
| handleItemActivate | Function | Marks an item active and calls onActivate (emits itemActivate on the component). |
| scrollToIndex | Function | Programmatic 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. |
| scrollToOffset | Function | Programmatic scroll to a pixel offset. |
| stopProgrammaticScroll | Function | Cancel any active smooth scroll animation. |
| handleScrollCorrection | Function | Adjust scroll position to compensate for measurement changes. |
scrollCorrection | Ref<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. |
| refresh | Function | Resets all measurements and state. |
| updateItemSize | Function | Register a manual item measurement. |
| updateItemSizes | Function | Register multiple manual item measurements. |
| updateHostOffset | Function | Force update the container's relative position. |
| updateDirection | Function | Manually trigger direction (LTR/RTL) detection. |
| getColumnWidth | Function | Helper to get a column's width. |
| getRowHeight | Function | Helper to get a row's height. |
| getRowOffset | Function | Helper to get a row's virtual offset (VU). |
| getColumnOffset | Function | Helper to get a column's virtual offset (VU). |
| getItemOffset | Function | Helper to get an item's virtual offset (VU). |
| getItemSize | Function | Helper to get an item's size along scroll axis (VU). |
| getRowIndexAt | Function | Helper to get the row (or item) index at a vertical virtual offset (VU). |
| getColumnIndexAt | Function | Helper 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
| Member | Type | Description |
|---|---|---|
itemSizesX / Y | FenwickTree | Prefix sum trees for item sizes. |
columnSizes | FenwickTree | Prefix sum tree for column widths. |
measuredItemsX / Y | Ref<Uint8Array> | Bitmask of measured items. |
measuredColumns | Ref<Uint8Array> | Bitmask of measured columns. |
treeUpdateFlag | Ref<number> | Reactive flag that increments when trees update. |
sizesInitialized | Ref<boolean> | True after initial sizes are calculated. |
| getItemBaseSize | Function | Helper to get item size from props. |
| getSizeAt | Function | Helper to get current size at index. |
| initializeSizes | Function | Setup trees from component props. |
| updateItemSizes | Function | Batch register measurements and trigger corrections. |
| refresh | Function | Reset 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
| Member | Type | Description |
|---|---|---|
trackProps | ComputedRef<object> | Attributes and listeners for the track element. Includes class and style. |
thumbProps | ComputedRef<object> | Attributes and listeners for the thumb element. Includes class and style. |
viewportPercent | ComputedRef<number> | Viewport size as percentage of total size (0-1). |
positionPercent | ComputedRef<number> | Scroll position as percentage of scrollable range (0-1). |
thumbSizePercent | ComputedRef<number> | Calculated thumb size (percentage of track, 0-100). |
thumbPositionPercent | ComputedRef<number> | Calculated thumb position (percentage of track, 0-100). |
isDragging | Ref<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
| Member | Type | Description |
|---|---|---|
isPointerScrolling | Ref<boolean> | True when user is actively dragging the content. |
| handlePointerDown / handlePointerMove / handlePointerUp | Function | Pointer event handlers to be bound to the scroll container. |
| handleWheel | Function | Wheel event handler to be bound to the scroll container. |
shiftOrigin | Function | Moves 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. |
| stopInertia | Function | Immediately 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
Endkey the composable requests extra range (endExtraX/endExtraYoptions) so the scroll clamp extends past the virtual content (the loading slot below the items). getLoadingSlotSize(optional): Height of the loading slot. When provided,Endincludes 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 throughactiveIndex/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/Spaceon the container, or an explicithandleItemActivatecall. Never called in'viewport'mode.- The engine handles it drives -
props,scrollDetails,scrollToIndex,stopProgrammaticScrolland 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/Endscroll to the start or end of the content.EndtargetstotalSize - viewportSize(plus the loading slot size whengetLoadingSlotSizeis 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/PageDownscroll 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/Spacedo nothing.
'item' mode moves a roving active item, announced through liveMessage:
- Arrows move the active item by one along the scroll axis (
ArrowLeft/ArrowRighton 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/Endmove 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/Spaceactivate the active item throughonActivate.
Return Value
| Member | Type | Description |
|---|---|---|
| handleKeyDown | Function | Keyboard event handler to be bound to the focusable scroll container. |
activeIndex | Ref<number> | Ref with the roving active item index, -1 when none. |
liveMessage | Ref<string> | Polite announcement for the active item, e.g. Item 21 of 100; empty when nothing is active. |
| setActiveIndex | (index: number | null) => void | Sets (null clears) the active item without scrolling, so a click or an external selection can be synced in. |
| handleItemActivate | (index: number) => void | Marks 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
| Member | Type | Description |
|---|---|---|
| setItemRef | Function | Callback 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/stylechanges. - 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
onScrollEndlifecycle event. - Resolves the snap target with the
resolveSnaputility. - Uses
scrollToIndexwithbehavior: '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/stickyStartYon 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
| Property | Type | Description |
|---|---|---|
onLoad | (axis: 'vertical' | 'horizontal', details: LoadDetails) => void | Callback 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 2 | number | While 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 0 | number | Extra 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
scrollDetailsreactively and reports the velocity and travel direction it observed toonLoad. - Respects the
loadDistanceandloadingprops from the component. - Prevents duplicate triggers while
loadingis true. - Skips the callback while the axis is flinging faster than
flingVelocity(default2VU/ms) and fires once it slows down. - Extends the threshold by
preload(VU, default0) 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
handleScrollCorrectionto 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
| Property | Type | Description |
|---|---|---|
storage Default 'memory' | 'memory' | 'session' | 'local' | Storage | Where 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' | string | Storage key used by 'session', 'local' and a custom Storage. Ignored in 'memory' mode. |
autoSave Default false | boolean | Save 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:
| Member | Type | Description |
|---|---|---|
save | () => ScrollSnapshot | Capture the current scroll position, keep it in memory and persist it when a storage is configured; returns the captured snapshot. |
restore | (snapshot?: ScrollSnapshot | null) => boolean | Scroll 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 | () => void | Drop 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
totalno 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
scaleXandscaleYfactors when total size exceeds browser limits. - Transparently maps physical scroll events to virtual positions.
- Automatically disabled when using
windowas the container (as the browser handles body scrolling differently).
API Reference
Types
ScrollDirection
'vertical' | 'horizontal' | 'both'Defines the virtualization axes for the VirtualScroll component.
ScrollDetails<T>
| Property | Type | Description |
|---|---|---|
items | RenderedItem<T>[] | Rendered items in the buffer. |
currentIndex | number | First visible row index below any sticky header. |
currentColIndex | number | First visible column index after any sticky column. |
currentEndIndex | number | Index of the last item visible above any sticky footer. |
currentEndColIndex | number | Index 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). |
isScrolling | boolean | Active scrolling state. |
isProgrammaticScroll | boolean | True if triggered by scrollToIndex/Offset. |
range | { start, end } | Range of currently rendered item indices, including the scroll buffer (inclusive start, exclusive end). |
columnRange | ColumnRange | Visible column range (grid). |
RenderedItem<T>
| Property | Type | Description |
|---|---|---|
item | T | The source data item. |
index | number | Item's position in the array. |
offset | { x, y } | Absolute pixel position within the wrapper (DU). |
size | { width, height } | Current dimensions (VU). |
originalX / originalY | number | Offsets before any sticky adjustments (VU). |
isSticky | boolean | Is configured as sticky. |
isStickyActive | boolean | Currently stuck to the edge. |
isStickyActiveX / isStickyActiveY | boolean | Currently stuck to the horizontal/vertical edge respectively. |
stickyOffset | { x, y } | Translation applied for sticky pushing effect (DU). |
ColumnRange
| Property | Type | Description |
|---|---|---|
start | number | Index of first rendered column. |
end | number | Index of last rendered column (exclusive). |
padStart | number | Pixel space to maintain before columns (VU). |
padEnd | number | Pixel space to maintain after columns (VU). |
LoadDetails
Second argument of the load event and of the infinite loading onLoad callback.
| Property | Type | Description |
|---|---|---|
velocity | number | Scroll velocity on the axis in virtual units (VU) per millisecond; 0 when it cannot be measured. |
direction | 'start' | 'end' | null | Direction 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.
| Property | Type | Description |
|---|---|---|
index | number | Index of the first visible item at the saved position. |
offset | number | Offset (VU) inside that item, measured from its start. |
total | number | Number 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).
| Property | Type | Description |
|---|---|---|
items | T[] | Data source. Required. |
itemSize | num | arr | fn | null | Sizing logic (fixed, circular array pattern, or function). Omitted/null measures rows; the estimate before measurement is 40px. |
direction | ScrollDirection | 'vertical' | 'horizontal' | 'both'. |
bufferBefore / bufferAfter | number | Items outside viewport. Default: 5. |
container | HTMLElement | Window | Scroll container. Defaults to component root. |
hostElement | HTMLElement | Reference for offset calculation (DU). |
ssrRange | SSRRange | Pre-rendered range for SSR. |
columnCount | number | Total columns for grid mode. |
columnWidth | num | arr | fn | null | Column sizing. Omitted/null measures columns; the estimate before measurement is 100px. |
scrollPaddingStart / End | num | {x, y} | Pixel offsets for scroll limits. |
gap / columnGap | number | Pixel space between items/cols. |
restoreScrollOnPrepend | boolean | Maintain 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. |
snap | SnapMode | Auto-alignment after scroll stop. |
initialScrollIndex | number | Mount-time jump index. |
initialScrollAlign | ScrollAlignment | Options | Alignment for initial jump. |
defaultItemSize | number | Estimate for dynamic items. |
defaultColumnWidth | number | Estimate for dynamic columns. |
debug | boolean | Enable 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.
| Property | Type | Description |
|---|---|---|
index | number | Item index. |
isSticky | boolean | Whether the item is configured as sticky. |
direction | ScrollDirection | Scroll direction. |
relativeScrollX / relativeScrollY | number | Virtual scroll position (VU). |
originalX / originalY | number | Virtual original position of the item (VU). |
width / height | number | Virtual item size (VU). |
stickyIndices | number[] | All configured sticky indices. |
fixedSize / fixedWidth | number | null | Fixed item size / column width (VU), null for dynamic. |
gap / columnGap | number | Item / column gap (VU). |
getItemQueryY / getItemQueryX | (index: number) => number | Prefix sum resolvers for offsets (VU). |
stickyStartX / stickyStartY | number | Size 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
| Property | Type | Description |
|---|---|---|
axis | ScrollAxis | Axis of the scrollbar. |
totalSize | number | Total size of content in pixels. |
position | number | Current scroll position in pixels. |
viewportSize | number | Visible area size in pixels. |
| scrollToOffset | (offset: number) => void | Callback to update position. |
containerId | string | ID for accessibility. |
isRtl | boolean | Enable RTL mapping. |
ariaLabel | string | Accessible label for the scrollbar. |
ScrollToIndexOptions
Full configuration for index-based scrolling.
| Property | Type | Description |
|---|---|---|
align | ScrollAlignment | Options | Where to align the item (default: 'auto'). |
behavior | 'auto' | 'smooth' | Scroll behavior (default: 'smooth'). |
ScrollAlignmentOptions
Allows axis-specific alignment in scrollToIndex.
| Property | Type | Description |
|---|---|---|
x | ScrollAlignment | Alignment on the horizontal axis. |
y | ScrollAlignment | Alignment on the vertical axis. |
ScrollAlignment
Controls the item's final position in the viewport during scrollToIndex.
| Value | Behavior |
|---|---|
'start' | Aligns to top (vertical) or left (horizontal) edge. |
'center' | Aligns to viewport center. |
'end' | Aligns to bottom (vertical) or right (horizontal) edge. |
'auto' Default | If 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.
| Value | Behavior |
|---|---|
false | No 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.
| Property | Type | Description |
|---|---|---|
props | VirtualScrollProps or a ref/getter | Virtual scroll configuration; pass a ref or getter when it is derived, so bounds, axis and column count stay live. |
isDynamicItemSize | boolean | Whether items have dynamic heights/widths. |
isDynamicColumnWidth | boolean | Whether columns have dynamic widths. |
defaultSize | number | Fallback size for items before they are measured. |
fixedItemSize | number | null | Fixed item size if applicable. |
direction | ScrollDirection | The scroll direction. |
FenwickTree
A Fenwick tree (binary indexed tree): O(log n) prefix sums and point updates.
| Method | Signature | Description |
|---|---|---|
update | (index, delta) => void | Update value at index and propagate changes. |
query | (index) => number | Get prefix sum up to index (exclusive). |
get | (index) => number | Get individual value at index. |
set | (index, value) => void | Set 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). |
length | number | Logical number of items in the tree (read-only property). |
findLowerBound | (value) => number | Find largest index where prefix sum <= value. |
rebuild | () => void | Rebuild tree from current values in O(n). |
resize | (size) => void | Resize tree while preserving values. |
shift | (offset) => void | Shift 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
): ScrollToIndexResultScrolls 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).
| Parameter | Type | Description |
|---|---|---|
rowIndex | number | null | Target row. null to keep current Y. Optional. |
colIndex | number | null | Target column. null to keep current X. Optional. |
options | Options | Alignment and behavior settings. |
Method scrollToOffset()
scrollToOffset(
x?: number | null,
y?: number | null,
options?: ScrollToOffsetOptions // { behavior?: 'auto' | 'smooth', endExtraX?: number, endExtraY?: number }, behavior default: 'auto'
): voidScrolls 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
): voidManually 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 }>): voidBatched 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): numberReturns the currently calculated width for a specific column index, taking measurements and gaps into account.
Method getRowHeight()
getRowHeight(index: number): numberReturns the currently calculated height for a specific row index, taking measurements and gaps into account.
Method getRowOffset()
getRowOffset(index: number): numberReturns the virtual vertical offset (top) of a row in virtual units (VU).
Method getColumnOffset()
getColumnOffset(index: number): numberReturns the virtual horizontal offset (left) of a column in virtual units (VU).
Method getItemOffset()
getItemOffset(index: number): numberReturns the virtual offset of an item along the scroll axis in virtual units (VU).
Method getItemSize()
getItemSize(index: number): numberReturns the size of an item along the scroll axis in virtual units (VU).
Method getRowIndexAt()
getRowIndexAt(offset: number): numberReturns the row (or item) index at a specific vertical (or horizontal in horizontal mode) virtual offset (VU).
Method getColumnIndexAt()
getColumnIndexAt(offset: number): numberReturns 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): voidApplies 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): numberReturns 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): numberQueries the size of an index from a Fenwick tree, honoring the configured size source, defaults, gaps and tree updates.
useVirtualScrollSizes initializeSizes()
initializeSizes(): voidRebuilds the size trees from the configured sizes and clears all measurement flags.
useVirtualScrollInertia handlePointerDown()
handlePointerDown(event: PointerEvent): voidStarts scaled drag/inertia handling on pointer down.
useVirtualScrollInertia handlePointerMove()
handlePointerMove(event: PointerEvent): voidTracks pointer movement while dragging (used by scaled touch/wheel inertia).
useVirtualScrollInertia handlePointerUp()
handlePointerUp(event: PointerEvent): voidEnds a drag sequence and launches inertia when needed.
useVirtualScrollInertia handleWheel()
handleWheel(event: WheelEvent): voidHandles wheel input when coordinate scaling is active so 1:1 movement is preserved.
useVirtualScrollInertia stopInertia()
stopInertia(): voidImmediately halts any running inertia animation.
useVirtualScrollKeyboard handleKeyDown()
handleKeyDown(event: KeyboardEvent): voidImplements keyboard navigation (arrows, Home/End, PageUp/PageDown) with alignment support.
useVirtualScrollObservers setItemRef()
setItemRef(el: unknown, index: number): voidCallback 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_SIZE40pxDEFAULT_COLUMN_WIDTH100pxDEFAULT_BUFFER5 itemsDEFAULT_MASONRY_TARGET_COLUMN_WIDTH240pxDEFAULT_MASONRY_MIN_COLUMNS1DEFAULT_MASONRY_MAX_COLUMNS10DEFAULT_MASONRY_GAP10pxDEFAULT_MASONRY_SEGMENT_SIZE500 itemsBROWSER_MAX_SIZE10,000,000pxEMPTY_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.