Skip to content
scroll logoScrollUI Performance
Lightweight, framework-agnostic virtual list engine with variable heights, sticky headers, grid support, and zero dependencies.
v0.0.16.1 KB gzipBrowser
createVirtualizercreateDomVirtualListcreateVirtualScrollercreateGroupedVirtualizercreateGridVirtualizercreateReactiveVirtualizercreateReactiveGroupedVirtualizercreateMeasurementCache +2 more →

Why Scroll?

Rendering thousands of items as real DOM nodes freezes the browser. Each node consumes layout, paint, and memory — long lists need to render only what is visible in the viewport.

ts
// Before — render all 10 000 items (browser freezes)
list.innerHTML = '';
items.forEach((item) => {
  const el = document.createElement('div');
  el.textContent = item.name;
  list.appendChild(el); // 10 000 DOM nodes
});

// After — Scroll (only ~15 visible rows in the DOM at any time)
import { createVirtualizer } from '@vielzeug/scroll';
const virt = createVirtualizer(scrollEl, {
  count: items.length,
  estimateSize: 36,
  onChange: ({ items, totalSize }) => {
    list.style.height = `${totalSize}px`;
    list.innerHTML = '';
    for (const { index, start } of items) {
      const el = document.createElement('div');
      el.style.cssText = `position:absolute;top:${start}px;height:36px;`;
      el.textContent = items[index].name;
      list.appendChild(el);
    }
  },
});
FeatureScrollTanStack Virtualreact-window
Bundle size6.1 KB~5 kB~8 kB
Framework agnosticReact only
Variable heights Measured Static
O(log n) lookup
using support
Zero dependencies

Use Scroll when you need to render large lists in a framework-agnostic environment with precise control over item measurement and scroll position.

Consider TanStack Virtual if you need its framework adapters and ecosystem integration.

Installation

sh
pnpm add @vielzeug/scroll
sh
npm install @vielzeug/scroll
sh
yarn add @vielzeug/scroll

Quick Start

ts
import { createVirtualizer } from '@vielzeug/scroll';

const scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;
const spacer = document.querySelector<HTMLElement>('.spacer')!;
const list = document.querySelector<HTMLElement>('.list')!;

const virt = createVirtualizer(scrollEl, {
  count: 10_000,
  estimateSize: 36,
  onChange: ({ items, totalSize }) => {
    // Stretch the container so the scrollbar reflects the full list
    spacer.style.height = `${totalSize}px`;
    list.innerHTML = '';

    for (const item of items) {
      const el = document.createElement('div');
      el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;`;
      el.textContent = `Row ${item.index}`;
      list.appendChild(el);
    }
  },
});

// Clean up
virt.dispose();

Entry Points

All APIs export from a single entry: @vielzeug/scroll.

Features

  • Framework-agnostic — callback-based onChange connects to any rendering layer (React, Vue, Svelte, Lit, vanilla DOM)
  • Fixed and variable heights — pass a fixed number, a per-index estimator function, or call measure() after rendering for exact heights
  • Batched measurements — calling measure() many times in a single tick coalesces into one prefix-sum rebuild via queueMicrotask
  • Stable-key reflow — call refresh() after reorder/filter changes to rebuild offsets without discarding measured sizes
  • Sticky headers — mark items with sticky to pin them at the viewport top; createGroupedVirtualizer handles section headers automatically
  • Grouped sectionscreateGroupedVirtualizer virtualizes sectioned data with per-section headers, onChange state, and scrollToSection/scrollToItem
  • Grid virtualizationcreateGridVirtualizer virtualizes two-dimensional data with independent row/column measurement and scrollToCell
  • Reactive integrationcreateReactiveVirtualizer and createReactiveGroupedVirtualizer expose state as a Signal compatible with @vielzeug/ripple
  • DOM adaptercreateDomVirtualList and createVirtualScroller manage virtualizer lifecycle, list-height styles, and DOM node pooling
  • Skipped re-rendersonChange is not called when a scroll event doesn't move the visible window across an item boundary
  • Programmatic scrollingscrollToIndex() with start, end, center, and auto alignment; scrollToOffset() for pixel control; scrollToRow()/scrollToColumn() for grids; all support behavior: 'smooth'
  • Horizontal + window targets — supports both element and window scrolling, in vertical or horizontal mode
  • Asymmetric overscan + gap — tune start/end overscan independently and add inter-item spacing
  • Atomic updatesvirt.update(...) lets you change count, estimator, overscan, and more in one call
  • Clamp-safescrollToIndex silently clamps out-of-range indices
  • Scroll state eventsonScrollingChange fires when scrolling starts/stops; onScrollEnd fires once scrolling settles (native scrollend or debounce fallback); isScrolling getter available at any time
  • Scroll anchor — viewport position is preserved visually when estimateSize changes via update()
  • Prepend supportprepend() adds items at the top while keeping the viewport visually stable
  • Disposable — implements [Symbol.dispose] for using declarations
  • Zero runtime dependencies (ripple is a peer dependency used only by createReactiveVirtualizer)

How It Works

Scroll maintains a prefix-sum offset array. On every scroll event it runs two binary searches — one for the first visible index, one for the last — to determine the render window in O(log n) time. Only the items within that window (plus overscan on each side) are passed to onChange.

text
Items:    [0]  [1]  [2]  [3]  [4]  [5]  [6]  ...
Offsets:   0   36   72  108  144  180  216  ...

scrollTop = 90, containerHeight = 120 → visible items 2–5
With overscan=3: render items 0–8

The offset array is rebuilt (O(n)) only when layout inputs change: on measure() flush, refresh(), update({ count }), update({ estimateSize }), or invalidate(). Scroll and resize events recompute the visible window without rebuilding offsets.

Documentation

See Also

  • Sigil — accessible web components that use Scroll internally for virtualized listboxes and comboboxes
  • Craft — web-component authoring layer; use with Scroll to build virtualizing custom elements
  • Dnd — drag-and-drop engine; combine with Scroll to make sortable virtual lists