@vielzeug/virtualit
v2.0.0
Published
> Lightweight, framework-agnostic virtual list engine for DOM rendering layers
Readme
@vielzeug/virtualit
Lightweight, framework-agnostic virtual list engine for DOM rendering layers
Virtualit renders only the items visible in the viewport plus a configurable overscan buffer. It uses a ResizeObserver for automatic container remeasurement and a passive scroll listener to keep the visible window in sync — no framework required.
Installation
pnpm add @vielzeug/virtualit
# npm install @vielzeug/virtualit
# yarn add @vielzeug/virtualitQuick Start
import { createVirtualizer } from '@vielzeug/virtualit';
const scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;
const virt = createVirtualizer(scrollEl, {
count: items.length,
estimateSize: 36,
onChange: (virtualItems, totalSize) => {
spacer.style.height = `${totalSize}px`;
list.innerHTML = '';
for (const item of virtualItems) {
const row = document.createElement('div');
row.style.cssText = `position:absolute;top:${item.top}px;left:0;right:0;height:${item.height}px;`;
row.textContent = items[item.index].label;
list.appendChild(row);
}
},
});
// Later:
virt.destroy();Features
- ✅ Framework-agnostic — callback-based
onChange; works with React, Vue, Svelte, Lit, or vanilla DOM - ✅ Fixed and variable heights — pass a number or a per-index estimator; call
measureElement()for exact heights - ✅ Batched measurements —
measureElement()calls within a single tick are coalesced into one rebuild viaqueueMicrotask - ✅ Skipped re-renders —
onChangeis not fired when a scroll event doesn't cross an item boundary - ✅ Programmatic scrolling —
scrollToIndex()withstart,end,center, andautoalignment;scrollToOffset()for pixel-level control; both supportbehavior: 'smooth' - ✅ Reactive count and density —
countandestimateSizesetters rebuild and re-render automatically - ✅ Typed Float64Array offsets — dense contiguous buffer for cache-friendly binary search
- ✅ Disposable — implements
[Symbol.dispose]forusingdeclarations - ✅ Zero dependencies
Usage
Vanilla DOM
import { createVirtualizer } from '@vielzeug/virtualit';
const scrollEl = document.getElementById('scroll')!;
const spacer = document.getElementById('spacer')!;
const list = document.getElementById('list')!;
const virt = createVirtualizer(scrollEl, {
count: 10_000,
estimateSize: 36,
overscan: 3,
onChange: (virtualItems, totalSize) => {
spacer.style.height = `${totalSize}px`;
list.innerHTML = '';
for (const item of virtualItems) {
const el = document.createElement('div');
el.style.cssText = `position:absolute;top:${item.top}px;`;
el.textContent = `Row ${item.index}`;
list.appendChild(el);
}
},
});Variable Heights
const virt = createVirtualizer(scrollEl, {
count: items.length,
estimateSize: (i) => (items[i].isHeader ? 48 : 36),
onChange: (virtualItems, totalSize) => {
// render items...
// After rendering, report the actual measured heights
for (const item of virtualItems) {
const el = list.querySelector(`[data-index="${item.index}"]`) as HTMLElement | null;
if (el) virt.measureElement(item.index, el.offsetHeight);
}
},
});Programmatic Scroll
// Jump to item 200, centered in viewport
virt.scrollToIndex(200, { align: 'center' });
// Smooth scroll to item 50
virt.scrollToIndex(50, { align: 'start', behavior: 'smooth' });
// Scroll to a known pixel offset
virt.scrollToOffset(1440, { behavior: 'smooth' });Updating the List
// Append more items — setter rebuilds and re-renders automatically
virt.count = newItems.length;
// Switch row density (e.g. compact ↔ comfortable view)
virt.estimateSize = isDense ? 32 : 48;
// Recompute after a font swap or layout shift
virt.invalidate();Explicit Resource Management
// `using` automatically calls virt.destroy() when the block exits
{
using virt = createVirtualizer(scrollEl, { count: 100 });
}API
Package Exports
export { Virtualizer, createVirtualizer } from '@vielzeug/virtualit';
export type { ScrollToIndexOptions, VirtualItem, VirtualizerOptions } from '@vielzeug/virtualit';createVirtualizer(el, options)
Creates and immediately attaches a Virtualizer to el.
| Parameter | Type | Description |
| ---------------------- | --------------------------------------------------- | ------------------------------------------------------ |
| el | HTMLElement | The scroll container to observe |
| options.count | number | Total number of items |
| options.estimateSize | number \| (i: number) => number | Row height estimate. Default: 36 |
| options.overscan | number | Items to render beyond the viewport edge. Default: 3 |
| options.onChange | (items: VirtualItem[], totalSize: number) => void | Called whenever the visible range changes |
Returns a Virtualizer instance.
Virtualizer
| Member | Type | Description |
| --------------------------- | ----------------------------------------- | -------------------------------------------------- |
| count | get/set number | Item count; setting rebuilds and re-renders |
| estimateSize | set number \| (i) => number | Update the size estimator; clears measured heights |
| attach(el) | (el: HTMLElement) => void | Attach (or re-attach) to a scroll container |
| destroy() | () => void | Remove all listeners; idempotent |
| [Symbol.dispose]() | — | Delegates to destroy() |
| getVirtualItems() | () => VirtualItem[] | Currently rendered items |
| getTotalSize() | () => number | Total scrollable height in px |
| measureElement(i, h) | (index: number, height: number) => void | Record an exact item height; batched per microtask |
| scrollToIndex(i, opts?) | (index, ScrollToIndexOptions) => void | Scroll to an item |
| scrollToOffset(px, opts?) | (offset, { behavior? }) => void | Scroll to a pixel offset |
| invalidate() | () => void | Clear all measured heights and re-render |
VirtualItem
interface VirtualItem {
index: number; // Position in the full list
top: number; // Pixel offset from the top of the scroll area
height: number; // Measured or estimated height
}Documentation
Full docs at vielzeug.dev/virtualit
| | | |---|---| | Usage Guide | Fixed/variable heights, overscan, scrolling | | API Reference | Complete type signatures | | Examples | Real-world virtual list patterns |
License
MIT © Helmuth Saatkamp — Part of the Vielzeug monorepo.
