@vielzeug/codex 3.1.0 → 26.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +124 -36
- package/data/llms-full.txt +2335 -679
- package/data/llms.txt +5 -3
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +30 -6
- package/data/packages/assay.json +11 -19
- package/data/packages/clockwork.json +30 -27
- package/data/packages/codex.json +4 -4
- package/data/packages/conduit.json +33 -34
- package/data/packages/courier.json +15 -14
- package/data/packages/dnd.json +2 -2
- package/data/packages/familiar.json +24 -3
- package/data/packages/flux.json +9 -6
- package/data/packages/focus.json +16 -6
- package/data/packages/gesture.json +10 -4
- package/data/packages/herald.json +7 -8
- package/data/packages/illusionist.json +1 -1
- package/data/packages/keymap.json +11 -7
- package/data/packages/ledger.json +25 -25
- package/data/packages/lingua.json +2 -2
- package/data/packages/mesh.json +29 -31
- package/data/packages/necromancer.json +3 -2
- package/data/packages/orbit.json +4 -3
- package/data/packages/ore.json +4 -3
- package/data/packages/postmaster.json +24 -24
- package/data/packages/pulse.json +28 -28
- package/data/packages/refine.json +6 -4
- package/data/packages/ripple.json +2 -2
- package/data/packages/sandbox.json +2 -2
- package/data/packages/scout.json +13 -7
- package/data/packages/scroll.json +7 -7
- package/data/packages/sentinel.json +26 -15
- package/data/packages/sigil.json +3 -3
- package/data/packages/spell.json +25 -30
- package/data/packages/tandem.json +29 -0
- package/data/packages/tavern.json +23 -0
- package/data/packages/tempo.json +44 -42
- package/data/packages/vault.json +9 -19
- package/data/packages/ward.json +26 -29
- package/data/packages/wayfinder.json +20 -13
- package/data/refine.json +4002 -3266
- package/data/search.json +198 -163
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -12
- package/skills/vielzeug/SKILL.md +2 -0
- package/dist/advanced.d.ts +0 -12
- package/dist/advanced.js +0 -13
- package/dist/advanced.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export type {\n DomVirtualListController,\n DomVirtualListOptions,\n DomVirtualListRenderArgs,\n RecycleFn,\n StickToBottomOptions,\n VirtualRenderItem,\n VirtualScrollerOptions,\n} from './dom-virtual-list';\nexport { createDomVirtualList, createVirtualScroller } from './dom-virtual-list';\nexport {
|
|
2
|
+
"apiSource": "export type {\n DomVirtualListController,\n DomVirtualListOptions,\n DomVirtualListRenderArgs,\n RecycleFn,\n StickToBottomOptions,\n VirtualRenderItem,\n VirtualScrollerOptions,\n} from './dom-virtual-list';\nexport { createDomVirtualList, createVirtualScroller } from './dom-virtual-list';\nexport { ScrollConfigError, ScrollError, ScrollRangeError } from './errors';\nexport type {\n GridRangeChangeEvent,\n GridVirtualizer,\n GridVirtualizerOptions,\n GridVirtualizerState,\n GridVirtualizerUpdateOptions,\n ScrollToCellOptions,\n} from './grid-virtualizer';\nexport { createGridVirtualizer } from './grid-virtualizer';\nexport type {\n GroupSection,\n GroupVirtualHeader,\n GroupVirtualItem,\n GroupVirtualizer,\n GroupVirtualizerOptions,\n GroupVirtualizerState,\n GroupVirtualizerUpdateOptions,\n} from './grouped-virtualizer';\nexport { createGroupedVirtualizer } from './grouped-virtualizer';\nexport type {\n MeasurementCache,\n Overscan,\n ScrollStore,\n ScrollTarget,\n ScrollToIndexOptions,\n VirtualItem,\n Virtualizer,\n VirtualizerOptions,\n VirtualizerState,\n VirtualizerUpdateOptions,\n VirtualKey,\n} from './virtualizer';\nexport { createMeasurementCache, createVirtualizer, DEFAULT_ESTIMATE_SIZE, DEFAULT_OVERSCAN } from './virtualizer';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Scroll — Virtual list engine for TypeScript\ndescription: Lightweight, dependency-free virtual list engine with variable heights, sticky headers, grids, and external-store integration.\npackage: scroll\ncategory: ui-performance\nkeywords: [virtual-list, virtualization, windowing, scroll, performance, large-lists]\nrelated: [dnd, ore, refine]\nexports:\n [\n createVirtualizer,\n createDomVirtualList,\n createVirtualScroller,\n createGroupedVirtualizer,\n createGridVirtualizer,\n createMeasurementCache,\n
|
|
5
|
-
"api": "---\ntitle: Scroll — API Reference\ndescription: Complete API reference for the Scroll virtual list engine.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------------- | -------------------------------------- | -------------- | ------------------------------------------------------------------------------------- |\n| `createVirtualizer()` | Core 1D virtualizer | Sync | `onChange` fires on construction — wire DOM first |\n| `createDomVirtualList()` | DOM adapter for dropdown/listbox UIs | Sync | Virtualizer is created lazily on first `setItems()` |\n| `createVirtualScroller()` | Self-contained scroller (creates DOM) | Sync | `dispose()` removes the generated scroll element |\n| `createGroupedVirtualizer()` | Sectioned list with sticky headers | Sync | `update()` preserves measured sizes — call `invalidate()` only on font/layout changes |\n| `createGridVirtualizer()` | Two-dimensional grid virtualizer | Sync | `onRangeChange` fires even when `onChange` is omitted |\n\n## Package Entry Point\n\nEverything exports from a single entry:\n\n```ts\nimport {\n createVirtualizer,\n createDomVirtualList,\n createVirtualScroller,\n createGroupedVirtualizer,\n createGridVirtualizer,\n createMeasurementCache,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n ScrollError,\n ScrollConfigurationError,\n ScrollRangeError,\n type Virtualizer,\n type VirtualItem,\n type VirtualizerState,\n type VirtualizerOptions,\n type VirtualizerUpdateOptions,\n type ScrollToIndexOptions,\n type Overscan,\n type VirtualKey,\n type MeasurementCache,\n type ScrollStore,\n type ScrollTarget,\n type DomVirtualListOptions,\n type DomVirtualListController,\n type DomVirtualListRenderArgs,\n type RecycleFn,\n type VirtualRenderItem,\n type StickToBottomOptions,\n type VirtualScrollerOptions,\n type GroupSection,\n type GroupVirtualizer,\n type GroupVirtualizerOptions,\n type GroupVirtualizerState,\n type GroupVirtualizerUpdateOptions,\n type GroupVirtualHeader,\n type GroupVirtualItem,\n type GridVirtualizer,\n type GridVirtualizerOptions,\n type GridVirtualizerState,\n type GridVirtualizerUpdateOptions,\n type GridRangeChangeEvent,\n type ScrollToCellOptions,\n} from '@vielzeug/scroll';\n```\n\n## `createVirtualizer(target, options)`\n\n```ts\ncreateVirtualizer(target: ScrollTarget, options: VirtualizerOptions): Virtualizer;\n```\n\nCreates and immediately attaches a virtualizer to the provided scroll container. `onChange` fires synchronously on construction with the initial visible window. Call `dispose()` on unmount.\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst rows = [{ label: 'Ada Lovelace' }, { label: 'Grace Hopper' }];\nconst scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;\nconst listEl = document.querySelector<HTMLElement>('.list')!;\n\nconst virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n gap: 8,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const row = document.createElement('div');\n row.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:${item.size}px;`;\n row.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(row);\n }\n },\n});\n```\n\n### Parameters\n\n| Parameter | Type | Description |\n| --------- | ----------------------- | --------------------------- |\n| `target` | `HTMLElement \\| Window` | Scroll container to observe |\n| `options` | `VirtualizerOptions` | Initial options |\n\n### `VirtualizerOptions`\n\n| Option | Type | Default | Description |\n| ------------------- | -------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------ |\n| `count` | `number` | required | Total item count |\n| `estimateSize` | `number \\| (index: number) => number` | `36` | Fixed size or per-index estimate in pixels |\n| `gap` | `number` | `0` | Gap between adjacent items in pixels |\n| `getItemKey` | `(index: number) => string \\| number` | `index => index` | Stable key for the measurement cache |\n| `horizontal` | `boolean` | `false` | Virtualize along the X axis instead of Y |\n| `initialOffset` | `number` | — | Initial scroll position; applied once on construction |\n| `keyboardScroll` | `boolean` | `false` | Enable keyboard navigation (Arrow/Page/Home/End keys) |\n| `autoMeasure` | `boolean` | `false` | Automatically measure visible items via ResizeObserver |\n| `measurementCache` | `MeasurementCache` | — | Shared external cache for scroll restoration or SSR pre-measurement |\n| `onChange` | `(state: VirtualizerState) => void` | — | Called when the visible window changes; replace through `update()`. |\n| `onScrollEnd` | `(offset: number) => void` | — | Called when scrolling settles; replace through `update()`. |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | — | Called when scroll activity starts or stops; replace through `update()`. |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Extra items outside the viewport; number = symmetric on both sides |\n| `scrollEndDelay` | `number` | `150` | Debounce delay (ms) used to detect scroll end when native `scrollend` is unavailable |\n| `sticky` | `(index: number) => boolean` | — | Mark an item as a sticky header (pinned at viewport top) |\n\nCallbacks and `scrollEndDelay` can be replaced through `update()`; `horizontal` and `initialOffset` remain construction-only.\n\n**Returns:** `Virtualizer`\n\n### `VirtualizerState`\n\n```ts\ninterface VirtualizerState {\n readonly items: VirtualItem[];\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n}\n```\n\n`items` contains the currently visible items plus overscan. `stickyItems` contains items marked sticky that are pinned at the viewport top.\n\n### `Virtualizer` — read-only properties\n\n| Property | Type | Description |\n| ---------------- | --------------- | ----------------------------------------------------------- |\n| `count` | `number` | Current item count |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called |\n| `disposed` | `boolean` | `true` after `dispose()` is called |\n| `isScrolling` | `boolean` | `true` while the user is scrolling; `false` once settled |\n| `items` | `VirtualItem[]` | Currently rendered items. Always populated. |\n| `scrollOffset` | `number` | Current scroll position in pixels |\n| `stickyItems` | `VirtualItem[]` | Items pinned at the viewport top (requires `sticky` option) |\n| `totalSize` | `number` | Total height (or width in horizontal mode) |\n\n### `Virtualizer` — methods\n\n| Method | Signature | Description |\n| ------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| `getSnapshot` | `() => VirtualizerState` | Return the current coherent state snapshot |\n| `subscribe` | `(listener: () => void) => () => void` | Observe state changes; returns an unsubscribe function |\n| `update` | `(next: VirtualizerUpdateOptions) => void` | Atomically update live options |\n| `measure` | `(index: number, size: number) => void` | Record one measured size; rebuild batched in microtask |\n| `measureBatch` | `(entries: Array<{ index: number; size: number }>) => void` | Record many sizes; single rebuild |\n| `measureEl` | `(index: number, el: HTMLElement) => () => void` | Attach ResizeObserver to auto-measure. Returns a disconnect function |\n| `refresh` | `() => void` | Rebuild offset table and re-emit; preserves cached measurements |\n| `prepend` | `(additionalCount: number) => void` | Add items at the top; adjusts scroll offset to keep viewport stable |\n| `scrollToIndex` | `(index: number, options?: ScrollToIndexOptions) => void` | Scroll to an item; out-of-range indices are clamped |\n| `scrollToOffset` | `(offset: number, options?: { behavior?: ScrollBehavior }) => void` | Scroll to a raw pixel offset |\n| `scrollToTop` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to offset `0` |\n| `scrollToBottom` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to the end of the list |\n| `isAtEnd` | `(threshold?: number) => boolean` | `true` when within `threshold` px (default `0`) of the end — check before appending items to decide whether to auto-follow (chat \"stick to bottom\") |\n| `invalidate` | `() => void` | Clear all measurements and rebuild from estimates |\n| `dispose` | `() => void` | Detach listeners; idempotent |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()` — enables `using` declarations |\n\n### `update(next)`\n\nAtomically updates one or more live options. Accepts: `autoMeasure`, `count`, `estimateSize`, `gap`, `getItemKey`, `keyboardScroll`, `measurementCache`, `onChange`, `onScrollEnd`, `onScrollingChange`, `overscan`, `scrollEndDelay`, and `sticky`. `horizontal` and `initialOffset` remain construction-only. Invalid static numeric values throw `ScrollConfigurationError` before any update applies.\n\nWhen `estimateSize` changes, the measurement cache is cleared and a scroll anchor is applied to keep the current viewport position visually stable.\n\n```ts\nvirt.update({ count: rows.length });\nvirt.update({ estimateSize: 40 });\nvirt.update({ gap: 8, overscan: { start: 5, end: 5 } });\n```\n\n### `measure(index, size)` and `measureBatch(entries)`\n\nReport exact sizes for variable-height rows. Calls within one microtask tick coalesce into a single offset rebuild. `measure()` is a no-op when the new size equals the current effective size.\n\n```ts\nvirt.measure(item.index, el.offsetHeight);\n\n// Prefer measureBatch for ResizeObserver batches\nvirt.measureBatch(entries.map((e) => ({ index: Number(e.target.dataset.index), size: e.contentRect.height })));\n```\n\n### `measureEl(index, el)`\n\nAttaches a `ResizeObserver` to auto-measure `el` on resize. Returns a disconnect function. The\nobserver is also disconnected automatically when the virtualizer is disposed, so calling the\nreturned function is only needed to stop observing a specific element early (e.g. before it is\nrecycled or removed).\n\n```ts\nconst disconnect = virt.measureEl(item.index, rowEl);\n// later: disconnect();\n```\n\n### `refresh()`\n\nRebuilds the full offset table and re-emits. Preserves cached measurements. Use after reordering, filtering, or any data change where sizes may have changed.\n\n### `prepend(additionalCount)`\n\nAdds `additionalCount` items at the front while adjusting scroll offset so the viewport stays visually stable. Use for \"load previous page\" patterns.\n\n### `scrollToIndex(index, options?)`\n\nScroll to an item. Out-of-range indices are clamped silently.\n\n| `align` | Behavior |\n| ------------------ | ------------------------------------------------------------ |\n| `'start'` | Item top at viewport top |\n| `'end'` | Item bottom at viewport bottom |\n| `'center'` | Item centered in the viewport |\n| `'auto'` (default) | No scroll if already fully visible; otherwise minimum scroll |\n\n```ts\nvirt.scrollToIndex(0, { align: 'start' });\nvirt.scrollToIndex(500, { align: 'center', behavior: 'smooth' });\nvirt.scrollToIndex(focusedIndex, { align: 'auto' });\n```\n\n### `scrollToOffset(offset, options?)`\n\n```ts\nvirt.scrollToOffset(Number(sessionStorage.getItem('scrollOffset') ?? '0'));\n```\n\n### `invalidate()`\n\nClears all measured sizes and rebuilds from estimator values.\n\n```ts\ndocument.fonts.ready.then(() => virt.invalidate());\n```\n\n### `dispose()` and `[Symbol.dispose]()`\n\n`dispose()` detaches observers and event listeners. It is idempotent.\n\n```ts\n{\n using virt = createVirtualizer(scrollEl, { count: rows.length, onChange: render });\n} // → dispose() called automatically\n```\n\n## `createDomVirtualList(options)`\n\n```ts\ncreateDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T>;\n```\n\nDOM-focused adapter. Manages virtualizer lifecycle, applies list-height styles automatically, and provides a node pool via `recycle`. The virtualizer is created lazily on the first non-empty `setItems()` call and destroyed automatically when `setItems([])` is called.\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\nconst ctrl = createDomVirtualList<Row>({\n estimateSize: 36,\n getItemKey: (_, row) => row.id,\n listElement: listEl,\n scrollElement: scrollEl,\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const el = recycle(item.data.id, () => document.createElement('div'));\n el.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);height:${item.size}px;`;\n el.textContent = item.data.label;\n listEl.appendChild(el);\n }\n },\n});\n\nctrl.setItems(rows);\nctrl.scrollToIndex(focusedIndex, { align: 'auto' });\nctrl.dispose();\n```\n\n### `DomVirtualListOptions<T>`\n\n| Option | Type | Default | Description |\n| ------------------ | --------------------------------------------- | -------- | ---------------------------------------------------------- |\n| `scrollElement` | `HTMLElement \\| Window` | required | Scroll container to observe |\n| `listElement` | `HTMLElement` | required | Element that receives height and item children |\n| `render` | `(args: DomVirtualListRenderArgs<T>) => void` | required | Called on every visible-window change |\n| `estimateSize` | `number \\| (index, item) => number` | `36` | Fixed or per-item size estimate |\n| `gap` | `number` | `0` | Gap between items in pixels |\n| `getItemKey` | `(index, item) => string \\| number` | — | Stable key; keeps measurements across `setItems()` calls |\n| `horizontal` | `boolean` | `false` | Virtualize along X axis |\n| `keyboardScroll` | `boolean` | `false` | Enable keyboard navigation (Arrow/Page/Home/End keys) |\n| `measurementCache` | `MeasurementCache` | — | External measurement cache |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Extra items outside the viewport; number = symmetric |\n| `sticky` | `(index: number, item: T) => boolean` | — | Mark items as sticky headers |\n| `clear` | `(listEl: HTMLElement) => void` | — | Custom teardown for listEl; defaults to `textContent = ''` |\n| `stickToBottom` | `boolean \\| StickToBottomOptions` | — | Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the end — the chat \"stick to bottom on new message\" pattern |\n\nWithout `getItemKey`, each `setItems()` call drops cached measurements.\n\n### `StickToBottomOptions`\n\n| Option | Type | Default | Description |\n| ----------- | --------- | ------- | --------------------------------------------------------------------------- |\n| `enabled` | `boolean` | `true` | Enable/disable at runtime — pass the object form to toggle without removing it |\n| `threshold` | `number` | `48` | Distance in pixels from the end still considered \"at the end\" |\n\n`stickToBottom` fires on **any** `setItems()` call made while the list is at the end — not just when the item count grows. This also follows a streaming last item that grows in place (same array length, bigger content) without you needing to detect that case yourself. It never fires while the user has scrolled away from the end, so reading older messages is never interrupted.\n\n```ts\nconst chat = createDomVirtualList<Message>({\n estimateSize: 48,\n getItemKey: (_, m) => m.id,\n listElement: listEl,\n render: renderMessages,\n scrollElement: scrollEl,\n stickToBottom: true, // or { threshold: 80 } for a larger \"still at bottom\" tolerance\n});\n\nchat.setItems(messages); // scrolls to bottom on first load\n// … later, a new message arrives (or the last one grows while streaming) …\nchat.setItems([...messages, newMessage]); // follows along only if the user was already at the bottom\n```\n\n### `DomVirtualListRenderArgs<T>`\n\n```ts\ntype DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>; // visible items — each has .data + layout fields\n listEl: HTMLElement;\n recycle: RecycleFn; // node pool — returns existing node or calls create()\n stickyItems: Array<VirtualRenderItem<T>>; // sticky items (requires sticky option)\n totalSize: number;\n};\n```\n\n`VirtualRenderItem<T>` is `VirtualItem` (`start`, `end`, `size`, `index`) enriched with `data: T`.\n\n`recycle(key, create)` returns a live node for `key` if one exists in the pool, or calls `create()` for a new one. Nodes not reused in a render cycle are removed automatically. `listEl.style.height` is set before `render` is called — you do not need to set it yourself.\n\n### `DomVirtualListController<T>`\n\nExtends `Virtualizer` (minus `prepend` and `update`) with `setItems()`. All virtualizer methods and live getters are available directly.\n\n| Member | Description |\n| ------------------ | ------------------------------------------------------------------------------------------- |\n| `getSnapshot()` | Return the current `VirtualizerState` snapshot |\n| `subscribe(fn)` | Observe state changes; works before the lazy virtualizer is created |\n| `setItems(items)` | Set the current item array. Spawns virtualizer on first non-empty call; destroys it on `[]` |\n| `count` | Current item count (live getter) |\n| `disposalSignal` | `AbortSignal` aborted on `dispose()` |\n| `isScrolling` | `true` while the user is scrolling; `false` once settled (live getter) |\n| `items` | Currently rendered virtual items (live getter) |\n| `totalSize` | Total list size in pixels (live getter) |\n| `scrollOffset` | Current scroll position (live getter) |\n| `stickyItems` | Sticky items pinned at viewport top (live getter) |\n| `measure` | Delegate to underlying virtualizer; no-op before first `setItems` |\n| `measureBatch` | Batch measurement delegate |\n| `measureEl` | Attach auto-measuring ResizeObserver |\n| `refresh` | Rebuild offset table and re-emit |\n| `invalidate` | Clear measurements and rebuild from estimates |\n| `scrollToIndex` | Scroll to an item |\n| `scrollToOffset` | Scroll to a pixel offset |\n| `scrollToTop` | Scroll to offset `0` |\n| `scrollToBottom` | Scroll to the end of the list |\n| `isAtEnd` | `true` when within `threshold` px of the end |\n| `dispose` | Teardown; idempotent |\n| `disposed` | `true` after `dispose()` is called (live getter) |\n| `[Symbol.dispose]` | Delegates to `dispose()` |\n\n## `createVirtualScroller(container, options)`\n\n```ts\ncreateVirtualScroller<T>(container: HTMLElement, options: VirtualScrollerOptions<T>): DomVirtualListController<T>;\n```\n\nCreates a scroll container `div` and inner list `div`, appends them to `container`, and returns a fully wired `DomVirtualListController`. Useful when the scroll DOM doesn't already exist.\n\n```ts\nconst list = createVirtualScroller<Row>(document.getElementById('root')!, {\n estimateSize: 36,\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const el = recycle(item.data.id, () => document.createElement('div'));\n el.textContent = item.data.label;\n el.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);`;\n listEl.appendChild(el);\n }\n },\n});\n\nlist.setItems(rows);\nlist.dispose(); // also removes the generated scroll container\n```\n\n`VirtualScrollerOptions<T>` is `DomVirtualListOptions<T>` minus `listElement`/`scrollElement`, plus:\n\n| Option | Type | Description |\n| ---------------- | -------- | ------------------------------------------------- |\n| `containerClass` | `string` | CSS class applied to the generated scroll element |\n\n`dispose()` removes the generated scroll container from the DOM.\n\n## `createGroupedVirtualizer(target, options)`\n\n```ts\ncreateGroupedVirtualizer<T>(target: ScrollTarget, options: GroupVirtualizerOptions<T>): GroupVirtualizer<T>;\n```\n\nVirtualizes a sectioned list. Headers are automatically sticky (pinned at viewport top while the section is in view).\n\n```ts\nimport { createGroupedVirtualizer } from '@vielzeug/scroll';\n\ntype Contact = { id: number; name: string };\n\nconst virt = createGroupedVirtualizer<Contact>(scrollEl, {\n estimateHeaderSize: 32,\n estimateItemSize: 48,\n sections: [\n { label: 'A', items: [{ id: 1, name: 'Alice' }] },\n { label: 'B', items: [{ id: 2, name: 'Bob' }] },\n ],\n onChange: ({ headers, items, stickyHeader, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n if (stickyHeader) {\n const el = document.createElement('div');\n el.className = 'sticky-header';\n el.textContent = stickyHeader.label;\n listEl.appendChild(el);\n }\n\n for (const header of headers) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${header.start}px;height:${header.size}px;`;\n el.textContent = header.label;\n listEl.appendChild(el);\n }\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;height:${item.size}px;`;\n el.textContent = item.data.name;\n listEl.appendChild(el);\n }\n },\n});\n\nvirt.scrollToSection(1, { align: 'start' });\nvirt.update(nextSections);\nvirt.dispose();\n```\n\n### `GroupVirtualizerOptions<T>`\n\n| Option | Type | Default | Description |\n| -------------------- | ------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------- |\n| `sections` | `Array<GroupSection<T>>` | required | Initial sections |\n| `onChange` | `(state: GroupVirtualizerState<T>) => void` | — | Called when the visible window changes; replace through `update()`. |\n| `onScrollEnd` | `(offset: number) => void` | — | Called when scrolling settles; replace through `update()`. |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | — | Called when scroll activity starts or stops; replace through `update()`. |\n| `estimateHeaderSize` | `number \\| (section, sectionIndex) => number` | `36` | Header height estimate |\n| `estimateItemSize` | `number \\| (item, itemIndex, sectionIndex) => number` | `36` | Item height estimate |\n| `getItemKey` | `(item: T, itemIndex: number, sectionIndex: number) => VirtualKey` | — | Stable key for measurement cache |\n| `horizontal` | `boolean` | `false` | Virtualize along X axis |\n| `measurementCache` | `MeasurementCache` | — | External measurement cache |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Overscan on each side (number = symmetric) |\n| `scrollEndDelay` | `number` | `150` | Debounce delay (ms) for scroll-end detection |\n\n### `GroupSection<T>`\n\n```ts\ninterface GroupSection<T> {\n items: T[];\n label: string;\n}\n```\n\n### `GroupVirtualizerState<T>`\n\n```ts\ninterface GroupVirtualizerState<T> {\n readonly headers: GroupVirtualHeader[];\n readonly items: Array<GroupVirtualItem<T>>;\n readonly stickyHeader: GroupVirtualHeader | null;\n readonly totalSize: number;\n}\n```\n\n`stickyHeader` is the header of the section currently at or above the viewport top, or `null` when at the very top. Render it as a floating overlay above the list.\n\n### `GroupVirtualItem<T>` and `GroupVirtualHeader`\n\n```ts\ninterface GroupVirtualItem<T> extends VirtualItem {\n data: T;\n itemIndex: number; // index within the section\n sectionIndex: number;\n}\n\ninterface GroupVirtualHeader extends VirtualItem {\n label: string;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualizer<T>` — methods\n\n`GroupVirtualizer<T>` is an independent interface that exposes all core virtualizer methods directly, plus grouped-specific navigation.\n\n| Method / Property | Description |\n| ---------------------------------- | ------------------------------------------------------------------------ |\n| `getSnapshot()` | Return the current `GroupVirtualizerState<T>` snapshot |\n| `subscribe(listener)` | Observe grouped state changes; returns an unsubscribe function |\n| `update(sections, opts?)` | Replace all sections with optional config overrides; see `GroupVirtualizerUpdateOptions<T>` |\n| `scrollToSection(i, options?)` | Scroll to section header at index `i`. Out-of-range is a no-op |\n| `scrollToItem(s, i, options?)` | Scroll to item `i` in section `s`. Out-of-range is a no-op |\n| `scrollToIndex(i, options?)` | Scroll to flat index `i` (from underlying virtualizer) |\n| `scrollToOffset(offset, options?)` | Scroll to a raw pixel offset |\n| `scrollToTop(options?)` | Scroll to offset `0` |\n| `scrollToBottom(options?)` | Scroll to the end of the list |\n| `measure(index, size)` | Record a measurement for a flat index |\n| `measureBatch(entries)` | Batch-record measurements for flat indices |\n| `measureEl(index, el)` | Attach auto-measuring ResizeObserver. Returns disconnect function |\n| `invalidate()` | Clear all measurements and rebuild |\n| `refresh()` | Rebuild offset table without clearing measurements |\n| `count` | Total flat item count (live getter) |\n| `disposalSignal` | `AbortSignal` aborted on `dispose()` |\n| `isScrolling` | `true` while the user is scrolling; `false` once scroll settles |\n| `items` | Currently rendered group items (live getter) |\n| `scrollOffset` | Current scroll position in pixels (live getter) |\n| `stickyItems` | Sticky items pinned at viewport top (live getter) |\n| `totalSize` | Total list size in pixels (live getter) |\n| `dispose()` | Teardown; idempotent |\n| `disposed` | `true` after `dispose()` is called |\n| `[Symbol.dispose]()` | Delegates to `dispose()` |\n\nAll scroll methods accept an optional `ScrollToIndexOptions` object (`{ align?, behavior?, onComplete? }`).\n\n### `GroupVirtualizerUpdateOptions<T>`\n\nPassed as the second argument to `groupVirtualizer.update()`. All fields are optional — omit any you don't want to change.\n\n| Option | Type | Description |\n| -------------------- | ------------------------------------------------------------- | -------------------------------------------------------- |\n| `estimateHeaderSize` | `number \\| (section, sectionIndex) => number` | New header size estimate, applied on next rebuild |\n| `estimateItemSize` | `number \\| (item, itemIndex, sectionIndex) => number` | New item size estimate, applied on next rebuild |\n| `getItemKey` | `(item, itemIndex, sectionIndex) => VirtualKey` | New item key function |\n| `measurementCache` | `MeasurementCache` | Hot-swap the measurement cache |\n| `onChange` | `(state: GroupVirtualizerState<T>) => void` | Replace the active onChange callback |\n| `onScrollEnd` | `(offset: number) => void` | Replace the active onScrollEnd callback |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | Replace the active onScrollingChange callback |\n| `overscan` | `number \\| { start?, end? }` | New overscan count |\n| `scrollEndDelay` | `number` | New debounce delay (ms) for scroll-end detection |\n\n> `horizontal` remains construction-only.\n\n## `createGridVirtualizer(target, options)`\n\n```ts\ncreateGridVirtualizer(target: ScrollTarget, options: GridVirtualizerOptions): GridVirtualizer;\n```\n\nTwo-dimensional virtualizer. Fires `onChange` with visible row and column descriptors. Callers form the cross-product `rows × cols` to render visible cells.\n\n```ts\nimport { createGridVirtualizer } from '@vielzeug/scroll';\n\nconst grid = createGridVirtualizer(scrollEl, {\n rowCount: 10_000,\n colCount: 50,\n estimateRowSize: 36,\n estimateColSize: 120,\n onChange: ({ rows, cols, totalHeight, totalWidth }) => {\n containerEl.style.cssText = `position:relative;height:${totalHeight}px;width:${totalWidth}px;`;\n containerEl.replaceChildren();\n\n for (const row of rows) {\n for (const col of cols) {\n const cell = document.createElement('div');\n cell.style.cssText = `position:absolute;top:${row.start}px;left:${col.start}px;height:${row.size}px;width:${col.size}px;`;\n cell.textContent = `${row.index},${col.index}`;\n containerEl.appendChild(cell);\n }\n }\n },\n});\n\ngrid.scrollToCell(500, 10, { rowAlign: 'center', colAlign: 'start' });\ngrid.dispose();\n```\n\n### `GridVirtualizerOptions`\n\n| Option | Type | Default | Description |\n| --------------------- | --------------------------------------- | ---------------------- | -------------------------------------- |\n| `rowCount` | `number` | required | Total row count |\n| `colCount` | `number` | required | Total column count |\n| `estimateRowSize` | `number \\| (row) => number` | `36` | Row height estimate |\n| `estimateColSize` | `number \\| (col) => number` | `36` | Column width estimate |\n| `rowGap` | `number` | `0` | Gap between rows |\n| `colGap` | `number` | `0` | Gap between columns |\n| `overscanY` | `{ start?: number; end?: number }` | `{ start: 3, end: 3 }` | Row overscan |\n| `overscanX` | `{ start?: number; end?: number }` | `{ start: 3, end: 3 }` | Column overscan |\n| `initialScrollTop` | `number` | — | Initial vertical scroll position |\n| `initialScrollLeft` | `number` | — | Initial horizontal scroll position |\n| `keyboardScroll` | `boolean` | `false` | Enable keyboard navigation (Arrow/Page/Home/End keys) |\n| `onChange` | `(state: GridVirtualizerState) => void` | — | Called when the visible window changes |\n| `onRangeChange` | `(range: GridRangeChangeEvent) => void` | — | Zero-allocation range callback |\n| `rowMeasurementCache` | `Map<number, number>` | — | External row measurement cache |\n| `colMeasurementCache` | `Map<number, number>` | — | External column measurement cache |\n\n### `GridVirtualizerState`\n\n```ts\ninterface GridVirtualizerState {\n readonly cols: VirtualItem[];\n readonly rows: VirtualItem[];\n readonly totalHeight: number;\n readonly totalWidth: number;\n}\n```\n\n### `GridVirtualizer` — properties and methods\n\n**Read-only properties:** `rows`, `cols`, `scrollTop`, `scrollLeft`, `totalHeight`, `totalWidth`, `disposalSignal`, `disposed`\n\n| Method | Description |\n| ---------------------------------- | --------------------------------------------------------------------------------- |\n| `getSnapshot()` | Return the current `GridVirtualizerState` snapshot |\n| `subscribe(listener)` | Observe grid state changes; returns an unsubscribe function |\n| `update(next)` | Atomically update row/col counts, estimates, gaps, and overscan |\n| `measureRow(row, size)` | Record a row height |\n| `measureColumn(col, size)` | Record a column width |\n| `measureBatch(rows, cols)` | Measure rows and columns in a single coordinated rebuild pass |\n| `measureRowEl(row, el)` | Auto-measure row height via ResizeObserver. Returns disconnect fn |\n| `measureColEl(col, el)` | Auto-measure column width via ResizeObserver. Returns disconnect fn |\n| `refresh()` | Rebuild offset tables from current measurements |\n| `invalidate()` | Clear all measurements and rebuild from estimates |\n| `scrollToCell(row, col, options?)` | Scroll to bring a cell into view; no-op when `rowCount === 0` or `colCount === 0` |\n| `scrollToRow(row, options?)` | Scroll to bring a row into view; `rowAlign` controls alignment |\n| `scrollToColumn(col, options?)` | Scroll to bring a column into view; `colAlign` controls alignment |\n| `prependRows(n)` | Add `n` rows at the top; adjusts scroll offset to keep viewport stable |\n| `dispose()` | Teardown; idempotent |\n| `[Symbol.dispose]()` | Delegates to `dispose()` |\n\n`measureRowEl`/`measureColEl`'s `ResizeObserver` is also disconnected automatically on `dispose()` —\nthe returned disconnect function is only needed to stop observing a specific element early.\n\n### `ScrollToCellOptions`\n\n```ts\ninterface ScrollToCellOptions {\n behavior?: ScrollBehavior;\n colAlign?: 'auto' | 'center' | 'end' | 'start';\n rowAlign?: 'auto' | 'center' | 'end' | 'start';\n}\n```\n\n## Types\n\n### `ScrollStore<State>`\n\n```ts\ninterface ScrollStore<State> {\n getSnapshot: () => State;\n subscribe: (listener: () => void) => () => void;\n}\n```\n\nFramework-neutral external-store contract implemented by every Scroll controller. `getSnapshot()` returns the current state. `subscribe()` observes later state changes and returns an unsubscribe function.\n\n### `VirtualItem`\n\n```ts\ninterface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n```\n\n### `VirtualizerState`\n\n```ts\ninterface VirtualizerState {\n readonly items: VirtualItem[];\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n}\n```\n\n### `ScrollToIndexOptions`\n\n```ts\ninterface ScrollToIndexOptions {\n align?: 'auto' | 'center' | 'end' | 'start';\n behavior?: ScrollBehavior;\n /** Called when the scroll animation completes (instant scrolls: next microtask). */\n onComplete?: () => void;\n}\n```\n\n### `Overscan`\n\n```ts\ntype Overscan = number | { end?: number; start?: number };\n```\n\nPassing a number is shorthand for symmetric overscan on both sides.\n\n### `VirtualKey`\n\n```ts\ntype VirtualKey = number | string;\n```\n\n### `VirtualRenderItem<T>`\n\n```ts\ntype VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n```\n\n### `ScrollTarget`\n\n```ts\ntype ScrollTarget = HTMLElement | Window;\n```\n\n### `MeasurementCache`\n\n```ts\ntype MeasurementCache = Map<VirtualKey, number>;\n```\n\nUse `createMeasurementCache()` to create an empty cache:\n\n```ts\nimport { createMeasurementCache } from '@vielzeug/scroll';\n\nconst cache = createMeasurementCache();\nconst virt1 = createVirtualizer(el1, { count: 100, measurementCache: cache });\nconst virt2 = createVirtualizer(el2, { count: 100, measurementCache: cache });\n```\n\n### `RecycleFn`\n\n```ts\ntype RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n```\n\n### `VirtualizerUpdateOptions`\n\n```ts\ninterface VirtualizerUpdateOptions {\n autoMeasure?: boolean;\n count?: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: ((index: number) => VirtualKey) | undefined;\n keyboardScroll?: boolean;\n /** Replace the active measurement cache. Existing entries are used immediately on the next rebuild. */\n measurementCache?: MeasurementCache;\n onChange?: ((state: VirtualizerState) => void) | undefined;\n onScrollEnd?: ((offset: number) => void) | undefined;\n onScrollingChange?: ((isScrolling: boolean) => void) | undefined;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sticky?: ((index: number) => boolean) | undefined;\n}\n```\n\n### `VirtualScrollerOptions<T>`\n\n`DomVirtualListOptions<T>` minus `listElement` and `scrollElement`, plus:\n\n```ts\ntype VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** CSS class applied to the generated scroll container element. */\n containerClass?: string;\n};\n```\n\n### `GridVirtualizerUpdateOptions`\n\n```ts\ninterface GridVirtualizerUpdateOptions {\n colCount?: number;\n colGap?: number;\n estimateColSize?: number | ((col: number) => number);\n estimateRowSize?: number | ((row: number) => number);\n keyboardScroll?: boolean;\n onChange?: ((state: GridVirtualizerState) => void) | undefined;\n onRangeChange?: ((range: GridRangeChangeEvent) => void) | undefined;\n overscanX?: Overscan;\n overscanY?: Overscan;\n rowCount?: number;\n rowGap?: number;\n}\n```\n\n### `GridRangeChangeEvent`\n\nFired by `onRangeChange` on `createGridVirtualizer`. Zero-allocation alternative to `onChange` — no `rows`/`cols` arrays are allocated.\n\n```ts\ninterface GridRangeChangeEvent {\n firstCol: number;\n firstRow: number;\n lastCol: number;\n lastRow: number;\n}\n```\n\n### `VirtualizerOptions`\n\n```ts\ninterface VirtualizerOptions {\n autoMeasure?: boolean;\n count: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: (index: number) => VirtualKey;\n horizontal?: boolean;\n initialOffset?: number;\n keyboardScroll?: boolean;\n measurementCache?: MeasurementCache;\n onChange?: (state: VirtualizerState) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sticky?: (index: number) => boolean;\n}\n```\n\n### `Virtualizer`\n\n```ts\ninterface Virtualizer extends ScrollStore<VirtualizerState> {\n readonly count: number;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n isAtEnd: (threshold?: number) => boolean;\n readonly isScrolling: boolean;\n readonly items: VirtualItem[];\n measure: (index: number, size: number) => void;\n measureBatch: (entries: Array<{ index: number; size: number }>) => void;\n measureEl: (index: number, el: HTMLElement) => () => void;\n prepend: (additionalCount: number) => void;\n refresh: () => void;\n readonly scrollOffset: number;\n scrollToBottom: (options?: { behavior?: ScrollBehavior }) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n scrollToOffset: (offset: number, options?: { behavior?: ScrollBehavior }) => void;\n scrollToTop: (options?: { behavior?: ScrollBehavior }) => void;\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n update: (next: VirtualizerUpdateOptions) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n### `StickToBottomOptions`\n\n```ts\ntype StickToBottomOptions = {\n enabled?: boolean;\n threshold?: number;\n};\n```\n\n### `DomVirtualListOptions<T>`\n\n```ts\ntype DomVirtualListOptions<T> = {\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n keyboardScroll?: boolean;\n listElement: HTMLElement;\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n stickToBottom?: boolean | StickToBottomOptions;\n sticky?: (index: number, item: T) => boolean;\n};\n```\n\n### `DomVirtualListController<T>`\n\n`Virtualizer` minus `prepend` and `update`, plus `setItems()`.\n\n```ts\ntype DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n```\n\n### `DomVirtualListRenderArgs<T>`\n\n```ts\ntype DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n```\n\n### `GroupSection<T>`\n\n```ts\ninterface GroupSection<T> {\n items: T[];\n label: string;\n}\n```\n\n### `GroupVirtualizerState<T>`\n\n```ts\ninterface GroupVirtualizerState<T> {\n readonly headers: GroupVirtualHeader[];\n readonly items: Array<GroupVirtualItem<T>>;\n readonly stickyHeader: GroupVirtualHeader | null;\n readonly totalSize: number;\n}\n```\n\n### `GroupVirtualItem<T>`\n\n```ts\ninterface GroupVirtualItem<T> extends VirtualItem {\n data: T;\n itemIndex: number;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualHeader`\n\n```ts\ninterface GroupVirtualHeader extends VirtualItem {\n label: string;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualizerOptions<T>`\n\n```ts\ninterface GroupVirtualizerOptions<T> {\n estimateHeaderSize?: number | ((section: GroupSection<T>, sectionIndex: number) => number);\n estimateItemSize?: number | ((item: T, itemIndex: number, sectionIndex: number) => number);\n getItemKey?: (item: T, itemIndex: number, sectionIndex: number) => VirtualKey;\n horizontal?: boolean;\n measurementCache?: MeasurementCache;\n onChange?: (state: GroupVirtualizerState<T>) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sections: Array<GroupSection<T>>;\n}\n```\n\n### `GroupVirtualizerUpdateOptions<T>`\n\n```ts\ninterface GroupVirtualizerUpdateOptions<T> {\n estimateHeaderSize?: number | ((section: GroupSection<T>, sectionIndex: number) => number);\n estimateItemSize?: number | ((item: T, itemIndex: number, sectionIndex: number) => number);\n getItemKey?: (item: T, itemIndex: number, sectionIndex: number) => VirtualKey;\n measurementCache?: MeasurementCache;\n onChange?: ((state: GroupVirtualizerState<T>) => void) | undefined;\n onScrollEnd?: ((offset: number) => void) | undefined;\n onScrollingChange?: ((isScrolling: boolean) => void) | undefined;\n overscan?: Overscan;\n scrollEndDelay?: number;\n}\n```\n\n### `GroupVirtualizer<T>`\n\n```ts\ninterface GroupVirtualizer<T> extends ScrollStore<GroupVirtualizerState<T>> {\n readonly count: number;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n readonly isScrolling: boolean;\n readonly items: ReadonlyArray<GroupVirtualItem<T>>;\n measure: (index: number, size: number) => void;\n measureBatch: (entries: Array<{ index: number; size: number }>) => void;\n measureEl: (index: number, el: HTMLElement) => () => void;\n refresh: () => void;\n readonly scrollOffset: number;\n scrollToBottom: (options?: { behavior?: ScrollBehavior }) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n scrollToItem: (sectionIndex: number, itemIndex: number, options?: ScrollToIndexOptions) => void;\n scrollToOffset: (offset: number, options?: { behavior?: ScrollBehavior }) => void;\n scrollToSection: (sectionIndex: number, options?: ScrollToIndexOptions) => void;\n scrollToTop: (options?: { behavior?: ScrollBehavior }) => void;\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n update: (sections: Array<GroupSection<T>>, opts?: GroupVirtualizerUpdateOptions<T>) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n### `GridVirtualizerState`\n\n```ts\ninterface GridVirtualizerState {\n readonly cols: VirtualItem[];\n readonly rows: VirtualItem[];\n readonly totalHeight: number;\n readonly totalWidth: number;\n}\n```\n\n### `ScrollToCellOptions`\n\n```ts\ninterface ScrollToCellOptions {\n behavior?: ScrollBehavior;\n colAlign?: 'auto' | 'center' | 'end' | 'start';\n rowAlign?: 'auto' | 'center' | 'end' | 'start';\n}\n```\n\n### `GridVirtualizerOptions`\n\n```ts\ninterface GridVirtualizerOptions {\n colCount: number;\n colGap?: number;\n colMeasurementCache?: Map<number, number>;\n estimateColSize?: number | ((col: number) => number);\n estimateRowSize?: number | ((row: number) => number);\n initialScrollLeft?: number;\n initialScrollTop?: number;\n keyboardScroll?: boolean;\n onChange?: (state: GridVirtualizerState) => void;\n onRangeChange?: (range: GridRangeChangeEvent) => void;\n overscanX?: Overscan;\n overscanY?: Overscan;\n rowCount: number;\n rowGap?: number;\n rowMeasurementCache?: Map<number, number>;\n}\n```\n\n### `GridVirtualizer`\n\n```ts\ninterface GridVirtualizer extends ScrollStore<GridVirtualizerState> {\n readonly cols: VirtualItem[];\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n measureBatch: (rows: Array<{ index: number; size: number }>, cols: Array<{ index: number; size: number }>) => void;\n measureColEl: (col: number, el: HTMLElement) => () => void;\n measureColumn: (col: number, size: number) => void;\n measureRow: (row: number, size: number) => void;\n measureRowEl: (row: number, el: HTMLElement) => () => void;\n prependRows: (additionalRowCount: number) => void;\n refresh: () => void;\n readonly rows: VirtualItem[];\n readonly scrollLeft: number;\n scrollToCell: (row: number, col: number, options?: ScrollToCellOptions) => void;\n scrollToColumn: (col: number, options?: Pick<ScrollToCellOptions, 'behavior' | 'colAlign'>) => void;\n readonly scrollTop: number;\n scrollToRow: (row: number, options?: Pick<ScrollToCellOptions, 'behavior' | 'rowAlign'>) => void;\n readonly totalHeight: number;\n readonly totalWidth: number;\n update: (next: GridVirtualizerUpdateOptions) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n## Errors\n\n| Class | Thrown when | Notable properties |\n| --- | --- | --- |\n| `ScrollError` | Base class for every Scroll error. | Use `instanceof ScrollError` to narrow unknown errors narrows errors from this package. |\n| `ScrollConfigurationError` | A constructor or `update()` receives invalid static configuration. | Extends `ScrollError`; malformed JavaScript values also use this class. |\n| `ScrollRangeError` | A DOM virtual-list render detects that a caller mutated its items array without calling `setItems()` again. | Extends `ScrollError`; message includes stale index and current item count. |\n\nRuntime estimator failures, stale measurements, and out-of-range navigation remain resilient: they fall back, no-op, or clamp as documented.\n\n### Constants\n\n```ts\nconst DEFAULT_ESTIMATE_SIZE = 36; // default estimateSize\nconst DEFAULT_OVERSCAN = 3; // default overscan on each side\n```\n",
|
|
6
|
-
"usage": "---\ntitle: Scroll — Usage Guide\ndescription: Fixed and variable heights, measurement, programmatic scrolling, and framework integration for Scroll.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRender only visible rows by passing a scroll container, a total item count, and a size estimate. Scroll calls `onChange` with the visible window whenever it changes.\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;\nconst listEl = document.querySelector<HTMLElement>('.list')!;\n\nconst virt = createVirtualizer(scrollEl, {\n count: 10_000,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = `Row ${item.index}`;\n listEl.appendChild(el);\n }\n },\n});\n\n// Cleanup\nvirt.dispose();\n```\n\n```html\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;position:relative;\">\n <div class=\"list\" style=\"position:relative;\"></div>\n</div>\n```\n\n## DOM Layout Requirements\n\nScroll uses **absolute positioning** for rendered items inside a relative container that stretches to the full list height. Your HTML needs three elements:\n\n```html\n<!-- 1. Scroll container — has a fixed height and overflow:auto/scroll -->\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;position:relative;\">\n <!-- 2. Spacer — height set to totalSize so the scrollbar is correct -->\n <div class=\"spacer\" style=\"position:relative;\">\n <!-- 3. Item container — items positioned absolutely inside here -->\n <div class=\"items\"></div>\n </div>\n</div>\n```\n\nA common alternative is to make the spacer and item container the same element:\n\n```html\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;\">\n <!-- Single relative container; items are absolute children -->\n <div class=\"list\" style=\"position:relative;\"></div>\n</div>\n```\n\n## DOM Adapter for Dropdowns and Listboxes\n\nIf your component already has a dropdown scroll container and a listbox element, use `createDomVirtualList`. It wraps the `Virtualizer` lifecycle and keeps the integration surface small. Items arrive as `VirtualRenderItem<T>` — a `VirtualItem` enriched with a `.data` field. Use `recycle` for efficient DOM node reuse.\n\nThe virtualizer is created lazily on the first non-empty `setItems()` call and destroyed automatically when `setItems([])` is called (clearing list styles in the process).\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\ntype Option = { disabled?: boolean; label: string; value: string };\n\nlet options: Option[] = [];\n\nconst domVirtualList = createDomVirtualList<Option>({\n estimateSize: 36,\n gap: 6,\n getItemKey: (_index, option) => option.value,\n listElement: listboxEl,\n overscan: { end: 4, start: 4 },\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const row = recycle(item.data.value, () => document.createElement('button'));\n row.type = 'button';\n row.className = 'option';\n row.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);height:${item.size}px;`;\n row.textContent = item.data.label;\n row.disabled = !!item.data.disabled;\n listEl.appendChild(row);\n }\n },\n scrollElement: dropdownEl,\n});\n\n// Keep in sync when options change\ndomVirtualList.setItems(options);\n\n// Open: setItems populates the list\n// Close: setItems([]) destroys the virtualizer and clears list styles\ndomVirtualList.setItems(isOpen ? options : []);\n\n// Keyboard nav\ndomVirtualList.scrollToIndex(focusedIndex, { align: 'auto' });\n\n// Component teardown\ndomVirtualList.dispose();\n```\n\nFor variable-height rows, pass `getItemKey` so measurements survive `setItems()` calls when items reorder or are filtered.\n\nWhen multiple sizes are available at once, use `measureBatch` to coalesce into a single rebuild:\n\n```ts\ndomVirtualList.measureBatch(\n entries.map((e) => ({ index: Number(e.target.dataset.index), size: e.contentRect.height })),\n);\n```\n\nUse `domVirtualList.invalidate()` to discard all cached measurements.\n\n## Fixed Heights\n\nPass a single number to `estimateSize` when all rows are the same height. This is the simplest and most performant case — the offset table never needs to be rebuilt during scrolling.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: 10_000,\n estimateSize: 36, // every row is 36px\n onChange: ({ items, totalSize }) => {\n list.style.height = `${totalSize}px`;\n list.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = data[item.index].name;\n list.appendChild(el);\n }\n },\n});\n```\n\n## Variable Heights — Estimator\n\nPass a **per-index function** to `estimateSize` when rows have predictable but non-uniform heights (e.g. group headers vs. regular rows). The offset table is built once at attach time using these estimates.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: flatList.length,\n estimateSize: (i) => (flatList[i].type === 'header' ? 48 : 36),\n onChange: ({ items, totalSize }) => {\n // render...\n },\n});\n```\n\n## Variable Heights — Measured\n\nFor truly dynamic heights (e.g. text wrapping, embedded images), render items at their estimated size first, then report the actual measured height with `measure()`. Scroll will coalesce all measurement calls within a single microtask tick into one offset rebuild.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 60, // initial estimate\n onChange: ({ items, totalSize }) => {\n list.style.height = `${totalSize}px`;\n list.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.dataset.index = String(item.index);\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;`;\n el.textContent = rows[item.index].body;\n list.appendChild(el);\n }\n\n // Measure after the DOM has painted\n requestAnimationFrame(() => {\n for (const item of items) {\n const el = list.querySelector<HTMLElement>(`[data-index=\"${item.index}\"]`);\n if (el) virt.measure(item.index, el.offsetHeight);\n }\n });\n },\n});\n```\n\n::: tip Measurement is idempotent\n`measure(index, height)` is a no-op when the new height matches the current effective height (measured or estimated). It is safe to call on every render without triggering unnecessary rebuilds.\n:::\n\n## Variable Heights — Batch Measurement\n\nWhen a `ResizeObserver` fires with multiple entries at once, use `measureBatch()` to apply all sizes in a single offset rebuild instead of triggering one rebuild per `measure()` call.\n\n```ts\nconst observer = new ResizeObserver((entries) => {\n virt.measureBatch(\n entries\n .filter((e) => e.target instanceof HTMLElement && e.target.dataset.index)\n .map((e) => ({\n index: Number((e.target as HTMLElement).dataset.index),\n size: e.contentRect.height,\n })),\n );\n});\n\n// Observe each rendered row\nfor (const item of virt.items) {\n const el = listEl.querySelector<HTMLElement>(`[data-index=\"${item.index}\"]`);\n if (el) observer.observe(el);\n}\n```\n\n## Overscan\n\n`overscan` controls how many extra items render outside the visible viewport on each side. Higher values reduce the chance of blank rows during fast scrolling; lower values keep the DOM smaller.\n\n```ts\ncreateVirtualizer(scrollEl, {\n count: 1_000,\n estimateSize: 36,\n overscan: 5, // symmetric shorthand — same as { start: 5, end: 5 } (default: 3)\n onChange: () => {\n /* ... */\n },\n});\n```\n\nAsymmetric overscan:\n\n```ts\ncreateVirtualizer(scrollEl, {\n count: 1_000,\n estimateSize: 36,\n overscan: { start: 8, end: 2 },\n onChange: () => {\n /* ... */\n },\n});\n```\n\n## Horizontal Lists\n\nSet `horizontal: true` to virtualize along the X axis.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: chips.length,\n estimateSize: 120,\n horizontal: true,\n onChange: ({ items, totalSize }) => {\n list.style.width = `${totalSize}px`;\n\n for (const item of items) {\n const chip = document.createElement('button');\n chip.style.cssText = `position:absolute;left:${item.start}px;top:0;width:${item.size}px;`;\n chip.textContent = chips[item.index].label;\n list.appendChild(chip);\n }\n },\n});\n```\n\n## Window Scroll Target\n\n`createVirtualizer` accepts `window` as the scroll target.\n\n```ts\nconst virt = createVirtualizer(window, {\n count: rows.length,\n estimateSize: 40,\n initialOffset: 320,\n onChange: ({ items, totalSize }) => {\n spacer.style.height = `${totalSize}px`;\n renderRows(items);\n },\n});\n```\n\n## Scroll State\n\nUse `virt.scrollOffset` to read the current scroll position at any time.\n\n```ts\nconst virt = createVirtualizer(scrollEl, { count: rows.length, estimateSize: 36, onChange: render });\n\n// Accessed outside onChange\nconsole.log(virt.scrollOffset);\n```\n\n## Updating Options\n\nWhen data or render strategy changes, call `update()` with one or more option fields. Updates apply atomically and trigger re-render when needed. Counts, gaps, and overscan must be finite non-negative integers; numeric size estimates must be finite positive values; offsets and `scrollEndDelay` must be finite non-negative numbers. Invalid constructor or `update()` values throw `ScrollConfigurationError` before any change applies.\n\nRuntime layout data stays resilient: estimator callbacks that throw or return invalid sizes fall back to the default estimate, stale measurements are ignored, and out-of-range navigation clamps or no-ops.\n\n```ts\n// Load more data\ndata.push(...newItems);\nvirt.update({ count: data.length });\n```\n\n```ts\n// Change multiple options together\nvirt.update({ count: data.length, overscan: { start: 5, end: 5 } });\n\n// Rebuild after reordering/filtering stable-key rows\nvirt.refresh();\n```\n\n## Switching Row Density\n\nUpdating `estimateSize` clears all previously measured heights, rebuilds offsets, and re-renders. This makes density switching (compact / comfortable / spacious views) straightforward.\n\n```ts\nfunction setDensity(mode: 'compact' | 'comfortable') {\n virt.update({ estimateSize: mode === 'compact' ? 32 : 48 });\n}\n```\n\n## Programmatic Scrolling\n\n### `scrollToIndex(index, options?)`\n\nScroll to bring a specific item into view.\n\n| `align` | Behaviour |\n| ------------------ | ------------------------------------------------------------------------ |\n| `'start'` | Item top aligns with the container top |\n| `'end'` | Item bottom aligns with the container bottom |\n| `'center'` | Item is centered in the viewport |\n| `'auto'` (default) | No scroll if already fully visible; otherwise scrolls the minimum amount |\n\n```ts\n// Jump to item 500 at the top of the viewport\nvirt.scrollToIndex(500, { align: 'start' });\n\n// Smooth-scroll to an item, centering it\nvirt.scrollToIndex(500, { align: 'center', behavior: 'smooth' });\n\n// Scroll only if the item is not already visible\nvirt.scrollToIndex(focusedIndex, { align: 'auto' });\n```\n\nOut-of-range indices are clamped silently: negative values scroll to item `0`, values ≥ `count` scroll to the last item.\n\n### `scrollToOffset(offset, options?)`\n\nScroll to an exact pixel position, useful for restoring a previously saved scroll state.\n\n```ts\n// Restore scroll position\nconst savedOffset = sessionStorage.getItem('scrollOffset');\nif (savedOffset) virt.scrollToOffset(Number(savedOffset));\n\n// Save on scroll\nscrollEl.addEventListener('scroll', () => {\n sessionStorage.setItem('scrollOffset', String(scrollEl.scrollTop));\n});\n```\n\n### `scrollToTop(options?)` / `scrollToBottom(options?)`\n\nConvenience wrappers to jump directly to the start or end of the list.\n\n```ts\n// Jump to the top\nvirt.scrollToTop();\n\n// Jump to the bottom with smooth scroll\nvirt.scrollToBottom({ behavior: 'smooth' });\n```\n\n### Chat \"stick to bottom on new message\"\n\n`createDomVirtualList`'s `stickToBottom` option automates the common chat/log pattern: follow new messages while the user is at the bottom, but never yank them away from history they scrolled up to read.\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\nconst chat = createDomVirtualList<Message>({\n estimateSize: 48,\n getItemKey: (_, m) => m.id,\n listElement: listEl,\n render: renderMessages,\n scrollElement: scrollEl,\n stickToBottom: true, // or { threshold: 80 } to widen the \"still at bottom\" tolerance\n});\n\nchat.setItems(messages);\n\n// New message arrives — follows only if the user hasn't scrolled up.\nsocket.on('message', (msg) => {\n messages = [...messages, msg];\n chat.setItems(messages);\n});\n```\n\nIt also follows a **streaming** last message that grows in place (tokens appended to the same message object, array length unchanged) — every `setItems()` call re-checks \"was the list at the end before this update?\", not just count changes. Build `isAtEnd()` from `createVirtualizer` directly for custom cases (e.g. showing a \"jump to latest\" button only while scrolled away):\n\n```ts\nconst showJumpButton = !virt.isAtEnd();\n```\n\n## Infinite Scroll — Loading More at the End\n\nUse `isAtEnd(threshold)` to fetch the next page as the user nears the bottom. `isAtEnd()` reports scroll position only — it keeps returning `true` while a fetch is in flight — so guard it with your own `loading` flag to avoid firing the same request twice.\n\n```ts\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\nlet rows = await fetchPage(0);\nlet loading = false;\n\nlet virt: Virtualizer;\nvirt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n\n if (!loading && virt.isAtEnd(200)) {\n loading = true;\n fetchPage(rows.length).then((nextRows) => {\n rows = [...rows, ...nextRows];\n virt.update({ count: rows.length });\n loading = false;\n });\n }\n },\n});\n```\n\n`isAtEnd(200)` fires once the viewport is within 200px of the bottom — tune the threshold to your row height and fetch latency. `loading` is the only guard needed: it's cleared once the new page lands, and `update({ count })` re-triggers `onChange`, which re-checks `isAtEnd()` against the new total on the next scroll.\n\n## Shared Measurement Cache\n\nWhen the same items are displayed across multiple virtualizer instances (e.g. a list and a detail panel that share row heights), pass a shared `MeasurementCache` created by `createMeasurementCache()`. Measurements recorded by one virtualizer are immediately available to all others using the same cache.\n\n```ts\nimport { createMeasurementCache, createVirtualizer } from '@vielzeug/scroll';\n\nconst cache = createMeasurementCache();\n\nconst listVirt = createVirtualizer(listScrollEl, {\n count: rows.length,\n estimateSize: 36,\n measurementCache: cache,\n onChange: renderList,\n});\n\nconst previewVirt = createVirtualizer(previewScrollEl, {\n count: rows.length,\n estimateSize: 36,\n measurementCache: cache,\n onChange: renderPreview,\n});\n\n// A measurement on listVirt is reflected in previewVirt immediately.\nlistVirt.measure(0, 72);\n```\n\nThe cache is a plain `Map<VirtualKey, number>` — you can pre-populate it from server data or persist it across sessions.\n\n```ts\n// Pre-populate from server-sent sizes\nconst cache = createMeasurementCache();\nfor (const { id, height } of serverSizes) cache.set(id, height);\n```\n\n## Invalidating Measurements\n\nCall `invalidate()` after an event that changes item heights without a data change — for example, a font load, a viewport width change that causes text to reflow, or toggling between a grid and list layout.\n\n```ts\ndocument.fonts.ready.then(() => virt.invalidate());\n```\n\nOn variable-height lists, `scrollToIndex()` uses the current estimate/measured cache. If you need an exact post-layout position after heights change, call `invalidate()` before scrolling again.\n\nFor same-length updates, call `setItems()` (DOM adapter) or `update()` (core). If the rendered height of rows changed, call `invalidate()` before scrolling again.\n\n## Lifecycle — create and dispose\n\n`createVirtualizer(el, options)` attaches immediately to the provided scroll container. If your container is replaced, dispose the old instance and create a new one.\n\n```ts\nlet virt = createVirtualizer(scrollContainerEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: render,\n});\n\nfunction remount(nextScrollContainerEl: HTMLElement) {\n virt.dispose();\n virt = createVirtualizer(nextScrollContainerEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: render,\n });\n}\n```\n\n`dispose()` is idempotent and safe to call multiple times.\n\n### Explicit Resource Management\n\n```ts\n// The `using` keyword calls virt.dispose() automatically at block exit\n{\n using virt = createVirtualizer(scrollEl, { count: rows.length, onChange: render });\n // ... use virt ...\n} // → virt.dispose() called here\n```\n\n## Keyboard Navigation\n\nEnable keyboard-based scrolling with the `keyboardScroll` option. Users can navigate lists using Arrow keys, Page Up/Down, Home, and End.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: 1000,\n estimateSize: 36,\n keyboardScroll: true, // Enable keyboard navigation\n onChange: render,\n});\n```\n\n**Supported keys:**\n- **Arrow Up/Down** (or Left/Right for horizontal lists) — Scroll by one estimated item height\n- **Page Up/Down** — Scroll by ~80% of viewport height\n- **Home** — Jump to the start of the list\n- **End** — Jump to the end of the list\n\n**Requirements:**\n- The scroll container (or a descendant) must have keyboard focus for events to fire\n- Works with all factories: `createVirtualizer`, `createDomVirtualList`, `createGroupedVirtualizer`, `createGridVirtualizer`\n- Arrow key step size is automatically calculated from your `estimateSize` (or `estimateRowSize`/`estimateColSize` for grids)\n\n## Auto-Measurement\n\nEnable automatic item measurement for dynamic or user-generated content that changes size. When `autoMeasure` is enabled, the virtualizer measures visible items via `ResizeObserver` and updates layout in real time.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: messages.length,\n estimateSize: 36, // Initial guess; will be measured\n autoMeasure: true, // Automatically measure visible items\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n // IMPORTANT: Set data-vz-key for auto-measure to find the element\n el.setAttribute('data-vz-key', String(item.index));\n el.textContent = messages[item.index]?.text ?? '';\n listEl.appendChild(el);\n }\n },\n});\n```\n\n**Requirements:**\n- Every rendered item must have a `data-vz-key` attribute with a unique value\n- Must use a DOM scroll target (not `Window`)\n- Elements must be in the DOM by the time `ResizeObserver` fires (usually the next microtask)\n\n**Use cases:**\n- Chat lists where messages expand on load\n- Expandable sections with collapsing text\n- Lazy-loaded thumbnails that arrive with unknown heights\n- User-resizable rows or dynamic content (videos, iframes)\n\n**Performance notes:**\n- Auto-measurement queries the DOM every render cycle — avoid with very large visible windows (100+ items)\n- For finer control, use the manual `measureEl()` method instead\n- Enable only on lists with truly variable-height items\n\n## Reactive Integration\n\nEvery Scroll controller is a framework-neutral external store. Read its current state with `getSnapshot()` and observe future changes with `subscribe()`.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: 1000,\n estimateSize: 36,\n});\n\nconst unsubscribe = virt.subscribe(() => {\n const { items, totalSize } = virt.getSnapshot();\n console.log(`Visible: ${items.length} items, total height: ${totalSize}px`);\n});\n\nunsubscribe();\nvirt.dispose();\n```\n\nBridge the same controller into Ripple without adding Ripple as a Scroll dependency:\n\n```ts\nimport { effect, fromSubscribable } from '@vielzeug/ripple';\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst virt = createVirtualizer(scrollEl, {\n count: 1000,\n estimateSize: 36,\n});\nconst scrollState = fromSubscribable(virt, { signal: virt.disposalSignal });\nconst renderEffect = effect(() => {\n const { items, totalSize } = scrollState.value;\n console.log(`Visible: ${items.length} items, total height: ${totalSize}px`);\n});\n\nrenderEffect.dispose();\nvirt.dispose();\n```\n\nThe same contract works with `createDomVirtualList()`, `createVirtualScroller()`, `createGroupedVirtualizer()`, and `createGridVirtualizer()`.\n\n## Framework Integration\n\nScroll is rendering-layer agnostic. The pattern is always the same: create the virtualizer when your scroll container is mounted, re-render your DOM in `onChange`, and call `dispose()` on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\nimport { useEffect, useLayoutEffect, useRef } from 'react';\n\ninterface Row {\n id: number;\n label: string;\n}\n\nfunction VirtualList({ rows }: { rows: Row[] }) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const virtRef = useRef<Virtualizer | null>(null);\n\n useEffect(() => {\n const scrollEl = scrollRef.current;\n const listEl = listRef.current;\n if (!scrollEl || !listEl) return;\n\n const virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n virtRef.current = virt;\n return () => virt.dispose();\n }, []); // attach once\n\n // useLayoutEffect, not useEffect: syncs count before paint. With useEffect,\n // the DOM (and anything reading `rows`) paints once with the new length before\n // the virtualizer's internal count catches up, which can render stale/out-of-bounds indices.\n useLayoutEffect(() => {\n virtRef.current?.update({ count: rows.length });\n }, [rows.length]);\n\n return (\n <div ref={scrollRef} style={{ height: 400, overflow: 'auto', position: 'relative' }}>\n <div ref={listRef} style={{ position: 'relative' }} />\n </div>\n );\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{ rows: { id: number; label: string }[] }>();\nconst scrollRef = ref<HTMLElement | null>(null);\nconst listRef = ref<HTMLElement | null>(null);\nlet virt: Virtualizer | null = null;\n\nonMounted(() => {\n if (!scrollRef.value || !listRef.value) return;\n const listEl = listRef.value;\n virt = createVirtualizer(scrollRef.value, {\n count: props.rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = props.rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n});\nwatch(\n () => props.rows.length,\n (n) => {\n virt?.update({ count: n });\n },\n);\nonUnmounted(() => virt?.dispose());\n</script>\n\n<template>\n <div ref=\"scrollRef\" style=\"height:400px;overflow:auto;position:relative;\">\n <div ref=\"listRef\" style=\"position:relative;\" />\n </div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\n let { rows }: { rows: { id: number; label: string }[] } = $props();\n let scrollEl: HTMLElement;\n let listEl: HTMLElement;\n let virt: Virtualizer;\n\n $effect(() => {\n virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n return () => virt.dispose();\n });\n\n $effect(() => { virt?.update({ count: rows.length }); });\n</script>\n\n<div bind:this={scrollEl} style=\"height:400px;overflow:auto;position:relative;\">\n <div bind:this={listEl} style=\"position:relative;\" />\n</div>\n```\n\n```ts [Web Components]\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\n@customElement('virtual-list')\nclass VirtualList extends LitElement {\n static styles = css`\n .scroll {\n height: 400px;\n overflow: auto;\n position: relative;\n }\n .list {\n position: relative;\n }\n `;\n\n @property({ type: Array }) rows: { label: string }[] = [];\n #virt: Virtualizer | null = null;\n\n firstUpdated() {\n const scrollEl = this.renderRoot.querySelector<HTMLElement>('.scroll')!;\n const listEl = this.renderRoot.querySelector<HTMLElement>('.list')!;\n this.#virt = createVirtualizer(scrollEl, {\n count: this.rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = this.rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n }\n\n updated() {\n this.#virt?.update({ count: this.rows.length });\n }\n disconnectedCallback() {\n this.#virt?.dispose();\n super.disconnectedCallback();\n }\n render() {\n return html`<div class=\"scroll\"><div class=\"list\"></div></div>`;\n }\n}\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Putting `rows` in the `useEffect` dependency array causes the virtualizer to be destroyed and recreated on every data update. Only include the scroll element reference. Call `virt.update({ count })` from a separate `useEffect` for data changes.\n- **React:** Use `useLayoutEffect`, not `useEffect`, for the `count`-sync effect. `useEffect` fires after paint — a new `count` can reach the DOM (e.g. via other state derived from `rows`) before `update({ count })` runs, rendering stale or out-of-bounds indices for one frame.\n- **Vue 3:** `ref.value` is `null` inside `setup()` — the DOM doesn't exist yet. Always create the virtualizer inside `onMounted`, not in `setup()`.\n- **Svelte:** In Svelte 5, `$effect` with `bind:this` runs after the DOM is painted. The `bind:this` variable is available when the `$effect` runs — no extra tick needed.\n- **Web Components:** `firstUpdated` fires once after the first render. Use `updated()` for subsequent prop changes — Lit calls it every time `rows` changes.\n\n## Working with Other Vielzeug Libraries\n\n### With Ore\n\nBuild a virtualizing custom element using Ore for the component shell and Scroll for the rendering engine.\n\n```ts\nimport { define, html, onMounted, ref } from '@vielzeug/ore';\nimport { createVirtualizer } from '@vielzeug/scroll';\n\ndefine('virtual-list', {\n setup() {\n const scrollRef = ref<HTMLElement>();\n const listRef = ref<HTMLElement>();\n\n onMounted(() => {\n if (!scrollRef.value || !listRef.value) return;\n const listEl = listRef.value;\n const virt = createVirtualizer(scrollRef.value, {\n count: 1000,\n estimateSize: 40,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const row = document.createElement('div');\n\n row.style.cssText = `position:absolute;top:${item.start}px;height:40px;`;\n row.textContent = `Row ${item.index}`;\n listEl.appendChild(row);\n }\n },\n },\n },\n });\n return () => virt.dispose();\n });\n\n return () => html`\n <div ref=${scrollRef} style=\"height:400px;overflow:auto;position:relative\">\n <div ref=${listRef} style=\"position:relative\"></div>\n </div>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Always provide `count` and `estimateSize` as a starting point, even for variable-height lists — measurements refine the estimates.\n- Call `dispose()` in the framework cleanup callback (useEffect return, onUnmounted, onDestroy) to free resize observers.\n- Use `overscan` to pre-render rows above and below the visible area to reduce blank flicker during fast scrolling.\n- Prefer `scrollToIndex()` with `align: 'start'` for programmatic navigation; use `align: 'center'` for focus management.\n- Use `createDomVirtualList()` for comboboxes, listboxes, and selects — it manages the virtualizer lifecycle and DOM node pooling for you.\n- Invalidate measurements with `invalidate()` when item content changes size (e.g., after expanding an accordion row).\n- For very large lists (>100k items), set a narrower `overscan` to limit DOM node count at any one time.\n- Use `refresh()` when item data or sizes may have changed; it rebuilds the offset table and re-emits.\n",
|
|
4
|
+
"index": "---\ntitle: Scroll — Virtual list engine for TypeScript\ndescription: Lightweight, dependency-free virtual list engine with variable heights, sticky headers, grids, and external-store integration.\npackage: scroll\ncategory: ui-performance\nkeywords: [virtual-list, virtualization, windowing, scroll, performance, large-lists]\nrelated: [dnd, ore, refine]\nexports:\n [\n createVirtualizer,\n createDomVirtualList,\n createVirtualScroller,\n createGroupedVirtualizer,\n createGridVirtualizer,\n createMeasurementCache,\n ScrollConfigError,\n ScrollError,\n ScrollRangeError,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"scroll\" />\n\n## Why Scroll?\n\nRendering 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.\n\n```ts\n// Before — render all 10 000 items (browser freezes)\nlist.replaceChildren();\nitems.forEach((item) => {\n const el = document.createElement('div');\n el.textContent = item.name;\n list.appendChild(el); // 10 000 DOM nodes\n});\n\n// After — Scroll (only ~15 visible rows in the DOM at any time)\nimport { createVirtualizer } from '@vielzeug/scroll';\nconst virtualizer = createVirtualizer(scrollEl, {\n count: items.length,\n estimateSize: 36,\n onChange: ({ items: visibleItems, totalSize }) => {\n list.style.height = `${totalSize}px`;\n list.replaceChildren();\n for (const { index, start } of visibleItems) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${start}px;height:36px;`;\n el.textContent = items[index].name;\n list.appendChild(el);\n }\n },\n});\n```\n\n| Feature | Scroll | TanStack Virtual | react-window |\n| ------------------ | --------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------- |\n| Bundle size | <PackageInfo package=\"scroll\" type=\"size\" /> | ~5 kB | ~8 kB |\n| Framework agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React only |\n| Variable heights | <ore-icon name=\"check\" size=\"16\"></ore-icon> Measured | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Static |\n| O(log n) lookup | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| `using` support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Scroll when** you need to render large lists in a framework-agnostic environment with precise control over item measurement and scroll position.\n\n**Consider TanStack Virtual** if you need its framework adapters and ecosystem integration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/scroll\n```\n\n```sh [npm]\nnpm install @vielzeug/scroll\n```\n\n```sh [yarn]\nyarn add @vielzeug/scroll\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;\nconst spacer = document.querySelector<HTMLElement>('.spacer')!;\nconst list = document.querySelector<HTMLElement>('.list')!;\n\nconst virt = createVirtualizer(scrollEl, {\n count: 10_000,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n // Stretch the container so the scrollbar reflects the full list\n spacer.style.height = `${totalSize}px`;\n list.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;`;\n el.textContent = `Row ${item.index}`;\n list.appendChild(el);\n }\n },\n});\n\n// Clean up\nvirt.dispose();\n```\n\n### Entry Points\n\nAll APIs export from a single entry: `@vielzeug/scroll`.\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Framework-agnostic** — callback-based `onChange` connects to any rendering layer (React, Vue, Svelte, Lit, vanilla DOM)\n- **Fixed and variable heights** — pass a fixed number, a per-index estimator function, or call `measure()` after rendering for exact heights\n- **Batched measurements** — calling `measure()` many times in a single tick coalesces into one prefix-sum rebuild via `queueMicrotask`\n- **Stable-key reflow** — call `refresh()` after reorder/filter changes to rebuild offsets without discarding measured sizes\n- **Sticky headers** — mark items with `sticky` to pin them at the viewport top; `createGroupedVirtualizer` handles section headers automatically\n- **Grouped sections** — `createGroupedVirtualizer` virtualizes sectioned data with per-section headers, `onChange` state, and `scrollToSection`/`scrollToItem`\n- **Grid virtualization** — `createGridVirtualizer` virtualizes two-dimensional data with independent row/column measurement and `scrollToCell`\n- **External-store state** — use `getSnapshot()` and `subscribe()` directly or bridge into Ripple and framework stores\n- **Keyboard navigation** — compose it from `scrollToIndex()` and `@vielzeug/keymap` instead of a built-in key handler\n- **Auto-measurement** — enable `autoMeasure` to automatically measure visible items via `ResizeObserver`\n- **DOM adapter** — `createDomVirtualList` and `createVirtualScroller` manage virtualizer lifecycle, list-height styles, and DOM node pooling\n- **Skipped re-renders** — `onChange` is not called when a scroll event doesn't move the visible window across an item boundary\n- **Programmatic scrolling** — `scrollToIndex()` with `start`, `end`, `center`, and `auto` alignment; `scrollToOffset()` for pixel control; `scrollToRow()`/`scrollToColumn()` for grids; all support `behavior: 'smooth'`\n- **Horizontal + window targets** — supports both element and `window` scrolling, in vertical or horizontal mode\n- **Asymmetric overscan + gap** — tune start/end overscan independently and add inter-item spacing\n- **Atomic updates** — `virt.update(...)` lets you change count, estimator, overscan, and more in one call\n- **Clamp-safe** — `scrollToIndex` silently clamps out-of-range indices\n- **Scroll state events** — `onScrollingChange` fires when scrolling starts/stops; `onScrollEnd` fires once scrolling settles (native `scrollend` or debounce fallback); `isScrolling` getter available at any time\n- **Scroll anchor** — viewport position is preserved visually when `estimateSize` changes via `update()`\n- **Prepend support** — `prepend()` adds items at the top while keeping the viewport visually stable\n- **Disposable** — implements `[Symbol.dispose]` for `using` declarations\n- `ScrollConfigError` — Rejects malformed static configuration before listeners attach or updates apply\n\n</div>\n\n## How It Works\n\nScroll 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`.\n\n```text\nItems: [0] [1] [2] [3] [4] [5] [6] ...\nOffsets: 0 36 72 108 144 180 216 ...\n\nscrollTop = 90, containerHeight = 120 → visible items 2–5\nWith overscan=3: render items 0–8\n```\n\nThe 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.\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — accessible web components that use Scroll internally for virtualized listboxes and comboboxes\n- [Ore](/ore/) — web-component authoring layer; use with Scroll to build virtualizing custom elements\n- [Dnd](/dnd/) — drag-and-drop engine; combine with Scroll to make sortable virtual lists\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Scroll — API Reference\ndescription: Complete API reference for the Scroll virtual list engine.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------------- | -------------------------------------- | -------------- | ------------------------------------------------------------------------------------- |\n| `createVirtualizer()` | Core 1D virtualizer | Sync | `onChange` fires on construction — wire DOM first |\n| `createDomVirtualList()` | DOM adapter for dropdown/listbox UIs | Sync | Virtualizer is created lazily on first `setItems()` |\n| `createVirtualScroller()` | Self-contained scroller (creates DOM) | Sync | `dispose()` removes the generated scroll element |\n| `createGroupedVirtualizer()` | Sectioned list with sticky headers | Sync | `update()` preserves measured sizes — call `invalidate()` only on font/layout changes |\n| `createGridVirtualizer()` | Two-dimensional grid virtualizer | Sync | `onRangeChange` fires even when `onChange` is omitted |\n\n## Package Entry Point\n\nEverything exports from a single entry:\n\n```ts\nimport {\n createVirtualizer,\n createDomVirtualList,\n createVirtualScroller,\n createGroupedVirtualizer,\n createGridVirtualizer,\n createMeasurementCache,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n ScrollError,\n ScrollConfigError,\n ScrollRangeError,\n type Virtualizer,\n type VirtualItem,\n type VirtualizerState,\n type VirtualizerOptions,\n type VirtualizerUpdateOptions,\n type ScrollToIndexOptions,\n type Overscan,\n type VirtualKey,\n type MeasurementCache,\n type ScrollStore,\n type ScrollTarget,\n type DomVirtualListOptions,\n type DomVirtualListController,\n type DomVirtualListRenderArgs,\n type RecycleFn,\n type VirtualRenderItem,\n type StickToBottomOptions,\n type VirtualScrollerOptions,\n type GroupSection,\n type GroupVirtualizer,\n type GroupVirtualizerOptions,\n type GroupVirtualizerState,\n type GroupVirtualizerUpdateOptions,\n type GroupVirtualHeader,\n type GroupVirtualItem,\n type GridVirtualizer,\n type GridVirtualizerOptions,\n type GridVirtualizerState,\n type GridVirtualizerUpdateOptions,\n type GridRangeChangeEvent,\n type ScrollToCellOptions,\n} from '@vielzeug/scroll';\n```\n\n## `createVirtualizer(target, options)`\n\n```ts\ncreateVirtualizer(target: ScrollTarget, options: VirtualizerOptions): Virtualizer;\n```\n\nCreates and immediately attaches a virtualizer to the provided scroll container. `onChange` fires synchronously on construction with the initial visible window. Call `dispose()` on unmount.\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst rows = [{ label: 'Ada Lovelace' }, { label: 'Grace Hopper' }];\nconst scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;\nconst listEl = document.querySelector<HTMLElement>('.list')!;\n\nconst virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n gap: 8,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const row = document.createElement('div');\n row.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:${item.size}px;`;\n row.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(row);\n }\n },\n});\n```\n\n### Parameters\n\n| Parameter | Type | Description |\n| --------- | ----------------------- | --------------------------- |\n| `target` | `HTMLElement \\| Window` | Scroll container to observe |\n| `options` | `VirtualizerOptions` | Initial options |\n\n### `VirtualizerOptions`\n\n| Option | Type | Default | Description |\n| ------------------- | -------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------ |\n| `count` | `number` | required | Total item count |\n| `estimateSize` | `number \\| (index: number) => number` | `36` | Fixed size or per-index estimate in pixels |\n| `gap` | `number` | `0` | Gap between adjacent items in pixels |\n| `getItemKey` | `(index: number) => string \\| number` | `index => index` | Stable key for the measurement cache |\n| `horizontal` | `boolean` | `false` | Virtualize along the X axis instead of Y |\n| `initialOffset` | `number` | — | Initial scroll position; applied once on construction |\n| `autoMeasure` | `boolean` | `false` | Automatically measure visible items via ResizeObserver |\n| `measurementCache` | `MeasurementCache` | — | Shared external cache for scroll restoration or SSR pre-measurement |\n| `onChange` | `(state: VirtualizerState) => void` | — | Called when the visible window changes; replace through `update()`. |\n| `onScrollEnd` | `(offset: number) => void` | — | Called when scrolling settles; replace through `update()`. |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | — | Called when scroll activity starts or stops; replace through `update()`. |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Extra items outside the viewport; number = symmetric on both sides |\n| `scrollEndDelay` | `number` | `150` | Debounce delay (ms) used to detect scroll end when native `scrollend` is unavailable |\n| `sticky` | `(index: number) => boolean` | — | Mark an item as a sticky header (pinned at viewport top) |\n\nCallbacks and `scrollEndDelay` can be replaced through `update()`; `horizontal` and `initialOffset` remain construction-only.\n\n**Returns:** `Virtualizer`\n\n### `VirtualizerState`\n\n```ts\ninterface VirtualizerState {\n readonly items: VirtualItem[];\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n}\n```\n\n`items` contains the currently visible items plus overscan. `stickyItems` contains items marked sticky that are pinned at the viewport top.\n\n### `Virtualizer` — read-only properties\n\n| Property | Type | Description |\n| ---------------- | --------------- | ----------------------------------------------------------- |\n| `count` | `number` | Current item count |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called |\n| `disposed` | `boolean` | `true` after `dispose()` is called |\n| `isScrolling` | `boolean` | `true` while the user is scrolling; `false` once settled |\n| `items` | `VirtualItem[]` | Currently rendered items. Always populated. |\n| `scrollOffset` | `number` | Current scroll position in pixels |\n| `stickyItems` | `VirtualItem[]` | Items pinned at the viewport top (requires `sticky` option) |\n| `totalSize` | `number` | Total height (or width in horizontal mode) |\n\n### `Virtualizer` — methods\n\n| Method | Signature | Description |\n| ------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| `getSnapshot` | `() => VirtualizerState` | Return the current coherent state snapshot |\n| `subscribe` | `(listener: () => void) => () => void` | Observe state changes; returns an unsubscribe function |\n| `update` | `(next: VirtualizerUpdateOptions) => void` | Atomically update live options |\n| `measure` | `(index: number, size: number) => void` | Record one measured size; rebuild batched in microtask |\n| `measureBatch` | `(entries: Array<{ index: number; size: number }>) => void` | Record many sizes; single rebuild |\n| `measureEl` | `(index: number, el: HTMLElement) => () => void` | Attach ResizeObserver to auto-measure. Returns a disconnect function |\n| `refresh` | `() => void` | Rebuild offset table and re-emit; preserves cached measurements |\n| `remeasure` | `() => void` | Re-read viewport size and scroll offset from the DOM and recompute |\n| `prepend` | `(additionalCount: number) => void` | Add items at the top; adjusts scroll offset to keep viewport stable |\n| `scrollToIndex` | `(index: number, options?: ScrollToIndexOptions) => void` | Scroll to an item; out-of-range indices are clamped |\n| `scrollToOffset` | `(offset: number, options?: { behavior?: ScrollBehavior }) => void` | Scroll to a raw pixel offset |\n| `scrollToTop` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to offset `0` |\n| `scrollToBottom` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to the end of the list |\n| `isAtEnd` | `(threshold?: number) => boolean` | `true` when within `threshold` px (default `0`) of the end — check before appending items to decide whether to auto-follow (chat \"stick to bottom\") |\n| `invalidate` | `() => void` | Clear all measurements and rebuild from estimates |\n| `dispose` | `() => void` | Detach listeners; idempotent |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()` — enables `using` declarations |\n\n### `update(next)`\n\nAtomically updates one or more live options. Accepts: `autoMeasure`, `count`, `estimateSize`, `gap`, `getItemKey`, `measurementCache`, `onChange`, `onScrollEnd`, `onScrollingChange`, `overscan`, `scrollEndDelay`, and `sticky`. `horizontal` and `initialOffset` remain construction-only. Invalid static numeric values throw `ScrollConfigError` before any update applies.\n\nWhen `estimateSize` changes, the measurement cache is cleared and a scroll anchor is applied to keep the current viewport position visually stable.\n\n```ts\nvirt.update({ count: rows.length });\nvirt.update({ estimateSize: 40 });\nvirt.update({ gap: 8, overscan: { start: 5, end: 5 } });\n```\n\n### `measure(index, size)` and `measureBatch(entries)`\n\nReport exact sizes for variable-height rows. Calls within one microtask tick coalesce into a single offset rebuild. `measure()` is a no-op when the new size equals the current effective size.\n\n```ts\nvirt.measure(item.index, el.offsetHeight);\n\n// Prefer measureBatch for ResizeObserver batches\nvirt.measureBatch(entries.map((e) => ({ index: Number(e.target.dataset.index), size: e.contentRect.height })));\n```\n\n### `measureEl(index, el)`\n\nAttaches a `ResizeObserver` to auto-measure `el` on resize. Returns a disconnect function. The\nobserver is also disconnected automatically when the virtualizer is disposed, so calling the\nreturned function is only needed to stop observing a specific element early (e.g. before it is\nrecycled or removed).\n\n```ts\nconst disconnect = virt.measureEl(item.index, rowEl);\n// later: disconnect();\n```\n\n### `refresh()`\n\nRebuilds the full offset table and re-emits. Preserves cached measurements. Use after reordering, filtering, or any data change where sizes may have changed.\n\n### `remeasure()`\n\nRe-reads the viewport size and scroll offset from the DOM and recomputes the visible window. A `display: none` container reports a zero viewport and the adapter's `ResizeObserver` never fires for the none→visible transition, so callers that toggle visibility (tabs, `v-show`) must call this when showing the container again.\n\n```ts\n// Re-showing a v-show/hidden scroll container\nvirt.remeasure();\n```\n\n### `prepend(additionalCount)`\n\nAdds `additionalCount` items at the front while adjusting scroll offset so the viewport stays visually stable. Use for \"load previous page\" patterns.\n\n### `scrollToIndex(index, options?)`\n\nScroll to an item. Out-of-range indices are clamped silently.\n\n| `align` | Behavior |\n| ------------------ | ------------------------------------------------------------ |\n| `'start'` | Item top at viewport top |\n| `'end'` | Item bottom at viewport bottom |\n| `'center'` | Item centered in the viewport |\n| `'auto'` (default) | No scroll if already fully visible; otherwise minimum scroll |\n\n```ts\nvirt.scrollToIndex(0, { align: 'start' });\nvirt.scrollToIndex(500, { align: 'center', behavior: 'smooth' });\nvirt.scrollToIndex(focusedIndex, { align: 'auto' });\n```\n\n### `scrollToOffset(offset, options?)`\n\n```ts\nvirt.scrollToOffset(Number(sessionStorage.getItem('scrollOffset') ?? '0'));\n```\n\n### `invalidate()`\n\nClears all measured sizes and rebuilds from estimator values.\n\n```ts\ndocument.fonts.ready.then(() => virt.invalidate());\n```\n\n### `dispose()` and `[Symbol.dispose]()`\n\n`dispose()` detaches observers and event listeners. It is idempotent.\n\n```ts\n{\n using virt = createVirtualizer(scrollEl, { count: rows.length, onChange: render });\n} // → dispose() called automatically\n```\n\n## `createDomVirtualList(options)`\n\n```ts\ncreateDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T>;\n```\n\nDOM-focused adapter. Manages virtualizer lifecycle, applies list-height styles automatically, and provides a node pool via `recycle`. The virtualizer is created lazily on the first non-empty `setItems()` call and destroyed automatically when `setItems([])` is called.\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\nconst ctrl = createDomVirtualList<Row>({\n estimateSize: 36,\n getItemKey: (_, row) => row.id,\n listElement: listEl,\n scrollElement: scrollEl,\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const el = recycle(item.data.id, () => document.createElement('div'));\n el.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);height:${item.size}px;`;\n el.textContent = item.data.label;\n listEl.appendChild(el);\n }\n },\n});\n\nctrl.setItems(rows);\nctrl.scrollToIndex(focusedIndex, { align: 'auto' });\nctrl.dispose();\n```\n\n### `DomVirtualListOptions<T>`\n\n| Option | Type | Default | Description |\n| ------------------ | --------------------------------------------- | -------- | ---------------------------------------------------------- |\n| `scrollElement` | `HTMLElement \\| Window` | required | Scroll container to observe |\n| `listElement` | `HTMLElement` | required | Element that receives height and item children |\n| `render` | `(args: DomVirtualListRenderArgs<T>) => void` | required | Called on every visible-window change |\n| `estimateSize` | `number \\| (index, item) => number` | `36` | Fixed or per-item size estimate |\n| `gap` | `number` | `0` | Gap between items in pixels |\n| `getItemKey` | `(index, item) => string \\| number` | — | Stable key; keeps measurements across `setItems()` calls |\n| `horizontal` | `boolean` | `false` | Virtualize along X axis |\n| `measurementCache` | `MeasurementCache` | — | External measurement cache |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Extra items outside the viewport; number = symmetric |\n| `sticky` | `(index: number, item: T) => boolean` | — | Mark items as sticky headers |\n| `clear` | `(listEl: HTMLElement) => void` | — | Custom teardown for listEl; defaults to `textContent = ''` |\n| `stickToBottom` | `boolean \\| StickToBottomOptions` | — | Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the end — the chat \"stick to bottom on new message\" pattern |\n\nWithout `getItemKey`, each `setItems()` call drops cached measurements.\n\n### `StickToBottomOptions`\n\n| Option | Type | Default | Description |\n| ----------- | --------- | ------- | --------------------------------------------------------------------------- |\n| `enabled` | `boolean` | `true` | Enable/disable at runtime — pass the object form to toggle without removing it |\n| `threshold` | `number` | `48` | Distance in pixels from the end still considered \"at the end\" |\n\n`stickToBottom` fires on **any** `setItems()` call made while the list is at the end — not just when the item count grows. This also follows a streaming last item that grows in place (same array length, bigger content) without you needing to detect that case yourself. It never fires while the user has scrolled away from the end, so reading older messages is never interrupted.\n\n```ts\nconst chat = createDomVirtualList<Message>({\n estimateSize: 48,\n getItemKey: (_, m) => m.id,\n listElement: listEl,\n render: renderMessages,\n scrollElement: scrollEl,\n stickToBottom: true, // or { threshold: 80 } for a larger \"still at bottom\" tolerance\n});\n\nchat.setItems(messages); // scrolls to bottom on first load\n// … later, a new message arrives (or the last one grows while streaming) …\nchat.setItems([...messages, newMessage]); // follows along only if the user was already at the bottom\n```\n\n### `DomVirtualListRenderArgs<T>`\n\n```ts\ntype DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>; // visible items — each has .data + layout fields\n listEl: HTMLElement;\n recycle: RecycleFn; // node pool — returns existing node or calls create()\n stickyItems: Array<VirtualRenderItem<T>>; // sticky items (requires sticky option)\n totalSize: number;\n};\n```\n\n`VirtualRenderItem<T>` is `VirtualItem` (`start`, `end`, `size`, `index`) enriched with `data: T`.\n\n`recycle(key, create)` returns a live node for `key` if one exists in the pool, or calls `create()` for a new one. Nodes not reused in a render cycle are removed automatically. `listEl.style.height` is set before `render` is called — you do not need to set it yourself.\n\n### `DomVirtualListController<T>`\n\nExtends `Virtualizer` (minus `prepend` and `update`) with `setItems()`. All virtualizer methods and live getters are available directly.\n\n| Member | Description |\n| ------------------ | ------------------------------------------------------------------------------------------- |\n| `getSnapshot()` | Return the current `VirtualizerState` snapshot |\n| `subscribe(fn)` | Observe state changes; works before the lazy virtualizer is created |\n| `setItems(items)` | Set the current item array. Spawns virtualizer on first non-empty call; destroys it on `[]` |\n| `count` | Current item count (live getter) |\n| `disposalSignal` | `AbortSignal` aborted on `dispose()` |\n| `isScrolling` | `true` while the user is scrolling; `false` once settled (live getter) |\n| `items` | Currently rendered virtual items (live getter) |\n| `totalSize` | Total list size in pixels (live getter) |\n| `scrollOffset` | Current scroll position (live getter) |\n| `stickyItems` | Sticky items pinned at viewport top (live getter) |\n| `measure` | Delegate to underlying virtualizer; no-op before first `setItems` |\n| `measureBatch` | Batch measurement delegate |\n| `measureEl` | Attach auto-measuring ResizeObserver |\n| `refresh` | Rebuild offset table and re-emit |\n| `remeasure` | Re-read viewport size and scroll offset from the DOM and recompute |\n| `invalidate` | Clear measurements and rebuild from estimates |\n| `scrollToIndex` | Scroll to an item |\n| `scrollToOffset` | Scroll to a pixel offset |\n| `scrollToTop` | Scroll to offset `0` |\n| `scrollToBottom` | Scroll to the end of the list |\n| `isAtEnd` | `true` when within `threshold` px of the end |\n| `dispose` | Teardown; idempotent |\n| `disposed` | `true` after `dispose()` is called (live getter) |\n| `[Symbol.dispose]` | Delegates to `dispose()` |\n\n## `createVirtualScroller(container, options)`\n\n```ts\ncreateVirtualScroller<T>(container: HTMLElement, options: VirtualScrollerOptions<T>): DomVirtualListController<T>;\n```\n\nCreates a scroll container `div` and inner list `div`, appends them to `container`, and returns a fully wired `DomVirtualListController`. Useful when the scroll DOM doesn't already exist.\n\n```ts\nconst list = createVirtualScroller<Row>(document.getElementById('root')!, {\n estimateSize: 36,\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const el = recycle(item.data.id, () => document.createElement('div'));\n el.textContent = item.data.label;\n el.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);`;\n listEl.appendChild(el);\n }\n },\n});\n\nlist.setItems(rows);\nlist.dispose(); // also removes the generated scroll container\n```\n\n`VirtualScrollerOptions<T>` is `DomVirtualListOptions<T>` minus `listElement`/`scrollElement`, plus:\n\n| Option | Type | Description |\n| ---------------- | -------- | ------------------------------------------------- |\n| `containerClass` | `string` | CSS class applied to the generated scroll element |\n\n`dispose()` removes the generated scroll container from the DOM.\n\n## `createGroupedVirtualizer(target, options)`\n\n```ts\ncreateGroupedVirtualizer<T>(target: ScrollTarget, options: GroupVirtualizerOptions<T>): GroupVirtualizer<T>;\n```\n\nVirtualizes a sectioned list. Headers are automatically sticky (pinned at viewport top while the section is in view).\n\n```ts\nimport { createGroupedVirtualizer } from '@vielzeug/scroll';\n\ntype Contact = { id: number; name: string };\n\nconst virt = createGroupedVirtualizer<Contact>(scrollEl, {\n estimateHeaderSize: 32,\n estimateItemSize: 48,\n sections: [\n { label: 'A', items: [{ id: 1, name: 'Alice' }] },\n { label: 'B', items: [{ id: 2, name: 'Bob' }] },\n ],\n onChange: ({ headers, items, stickyHeader, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n if (stickyHeader) {\n const el = document.createElement('div');\n el.className = 'sticky-header';\n el.textContent = stickyHeader.label;\n listEl.appendChild(el);\n }\n\n for (const header of headers) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${header.start}px;height:${header.size}px;`;\n el.textContent = header.label;\n listEl.appendChild(el);\n }\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;height:${item.size}px;`;\n el.textContent = item.data.name;\n listEl.appendChild(el);\n }\n },\n});\n\nvirt.scrollToSection(1, { align: 'start' });\nvirt.update(nextSections);\nvirt.dispose();\n```\n\n### `GroupVirtualizerOptions<T>`\n\n| Option | Type | Default | Description |\n| -------------------- | ------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------- |\n| `sections` | `Array<GroupSection<T>>` | required | Initial sections |\n| `onChange` | `(state: GroupVirtualizerState<T>) => void` | — | Called when the visible window changes; replace through `update()`. |\n| `onScrollEnd` | `(offset: number) => void` | — | Called when scrolling settles; replace through `update()`. |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | — | Called when scroll activity starts or stops; replace through `update()`. |\n| `estimateHeaderSize` | `number \\| (section, sectionIndex) => number` | `36` | Header height estimate |\n| `estimateItemSize` | `number \\| (item, itemIndex, sectionIndex) => number` | `36` | Item height estimate |\n| `getItemKey` | `(item: T, itemIndex: number, sectionIndex: number) => VirtualKey` | — | Stable key for measurement cache |\n| `horizontal` | `boolean` | `false` | Virtualize along X axis |\n| `measurementCache` | `MeasurementCache` | — | External measurement cache |\n| `overscan` | `number \\| { start?: number; end?: number }` | `3` | Overscan on each side (number = symmetric) |\n| `scrollEndDelay` | `number` | `150` | Debounce delay (ms) for scroll-end detection |\n\n### `GroupSection<T>`\n\n```ts\ninterface GroupSection<T> {\n items: T[];\n label: string;\n}\n```\n\n### `GroupVirtualizerState<T>`\n\n```ts\ninterface GroupVirtualizerState<T> {\n readonly headers: GroupVirtualHeader[];\n readonly items: Array<GroupVirtualItem<T>>;\n readonly stickyHeader: GroupVirtualHeader | null;\n readonly totalSize: number;\n}\n```\n\n`stickyHeader` is the header of the section currently at or above the viewport top, or `null` when at the very top. Render it as a floating overlay above the list.\n\n### `GroupVirtualItem<T>` and `GroupVirtualHeader`\n\n```ts\ninterface GroupVirtualItem<T> extends VirtualItem {\n data: T;\n itemIndex: number; // index within the section\n sectionIndex: number;\n}\n\ninterface GroupVirtualHeader extends VirtualItem {\n label: string;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualizer<T>` — methods\n\n`GroupVirtualizer<T>` is an independent interface that exposes all core virtualizer methods directly, plus grouped-specific navigation.\n\n| Method / Property | Description |\n| ---------------------------------- | ------------------------------------------------------------------------ |\n| `getSnapshot()` | Return the current `GroupVirtualizerState<T>` snapshot |\n| `subscribe(listener)` | Observe grouped state changes; returns an unsubscribe function |\n| `update(sections, opts?)` | Replace all sections with optional config overrides; see `GroupVirtualizerUpdateOptions<T>` |\n| `scrollToSection(i, options?)` | Scroll to section header at index `i`. Out-of-range is a no-op |\n| `scrollToItem(s, i, options?)` | Scroll to item `i` in section `s`. Out-of-range is a no-op |\n| `scrollToIndex(i, options?)` | Scroll to flat index `i` (from underlying virtualizer) |\n| `scrollToOffset(offset, options?)` | Scroll to a raw pixel offset |\n| `scrollToTop(options?)` | Scroll to offset `0` |\n| `scrollToBottom(options?)` | Scroll to the end of the list |\n| `measure(index, size)` | Record a measurement for a flat index |\n| `measureBatch(entries)` | Batch-record measurements for flat indices |\n| `measureEl(index, el)` | Attach auto-measuring ResizeObserver. Returns disconnect function |\n| `invalidate()` | Clear all measurements and rebuild |\n| `refresh()` | Rebuild offset table without clearing measurements |\n| `count` | Total flat item count (live getter) |\n| `disposalSignal` | `AbortSignal` aborted on `dispose()` |\n| `isScrolling` | `true` while the user is scrolling; `false` once scroll settles |\n| `items` | Currently rendered group items (live getter) |\n| `scrollOffset` | Current scroll position in pixels (live getter) |\n| `stickyItems` | Sticky items pinned at viewport top (live getter) |\n| `totalSize` | Total list size in pixels (live getter) |\n| `dispose()` | Teardown; idempotent |\n| `disposed` | `true` after `dispose()` is called |\n| `[Symbol.dispose]()` | Delegates to `dispose()` |\n\nAll scroll methods accept an optional `ScrollToIndexOptions` object (`{ align?, behavior?, onComplete? }`).\n\n### `GroupVirtualizerUpdateOptions<T>`\n\nPassed as the second argument to `groupVirtualizer.update()`. All fields are optional — omit any you don't want to change.\n\n| Option | Type | Description |\n| -------------------- | ------------------------------------------------------------- | -------------------------------------------------------- |\n| `estimateHeaderSize` | `number \\| (section, sectionIndex) => number` | New header size estimate, applied on next rebuild |\n| `estimateItemSize` | `number \\| (item, itemIndex, sectionIndex) => number` | New item size estimate, applied on next rebuild |\n| `getItemKey` | `(item, itemIndex, sectionIndex) => VirtualKey` | New item key function |\n| `measurementCache` | `MeasurementCache` | Hot-swap the measurement cache |\n| `onChange` | `(state: GroupVirtualizerState<T>) => void` | Replace the active onChange callback |\n| `onScrollEnd` | `(offset: number) => void` | Replace the active onScrollEnd callback |\n| `onScrollingChange` | `(isScrolling: boolean) => void` | Replace the active onScrollingChange callback |\n| `overscan` | `number \\| { start?, end? }` | New overscan count |\n| `scrollEndDelay` | `number` | New debounce delay (ms) for scroll-end detection |\n\n> `horizontal` remains construction-only.\n\n## `createGridVirtualizer(target, options)`\n\n```ts\ncreateGridVirtualizer(target: ScrollTarget, options: GridVirtualizerOptions): GridVirtualizer;\n```\n\nTwo-dimensional virtualizer. Fires `onChange` with visible row and column descriptors. Callers form the cross-product `rows × cols` to render visible cells.\n\n```ts\nimport { createGridVirtualizer } from '@vielzeug/scroll';\n\nconst grid = createGridVirtualizer(scrollEl, {\n rowCount: 10_000,\n colCount: 50,\n estimateRowSize: 36,\n estimateColSize: 120,\n onChange: ({ rows, cols, totalHeight, totalWidth }) => {\n containerEl.style.cssText = `position:relative;height:${totalHeight}px;width:${totalWidth}px;`;\n containerEl.replaceChildren();\n\n for (const row of rows) {\n for (const col of cols) {\n const cell = document.createElement('div');\n cell.style.cssText = `position:absolute;top:${row.start}px;left:${col.start}px;height:${row.size}px;width:${col.size}px;`;\n cell.textContent = `${row.index},${col.index}`;\n containerEl.appendChild(cell);\n }\n }\n },\n});\n\ngrid.scrollToCell(500, 10, { rowAlign: 'center', colAlign: 'start' });\ngrid.dispose();\n```\n\n### `GridVirtualizerOptions`\n\n| Option | Type | Default | Description |\n| --------------------- | --------------------------------------- | ---------------------- | -------------------------------------- |\n| `rowCount` | `number` | required | Total row count |\n| `colCount` | `number` | required | Total column count |\n| `estimateRowSize` | `number \\| (row) => number` | `36` | Row height estimate |\n| `estimateColSize` | `number \\| (col) => number` | `36` | Column width estimate |\n| `rowGap` | `number` | `0` | Gap between rows |\n| `colGap` | `number` | `0` | Gap between columns |\n| `overscanY` | `{ start?: number; end?: number }` | `{ start: 3, end: 3 }` | Row overscan |\n| `overscanX` | `{ start?: number; end?: number }` | `{ start: 3, end: 3 }` | Column overscan |\n| `initialScrollTop` | `number` | — | Initial vertical scroll position |\n| `initialScrollLeft` | `number` | — | Initial horizontal scroll position |\n| `onChange` | `(state: GridVirtualizerState) => void` | — | Called when the visible window changes |\n| `onRangeChange` | `(range: GridRangeChangeEvent) => void` | — | Zero-allocation range callback |\n| `rowMeasurementCache` | `Map<number, number>` | — | External row measurement cache |\n| `colMeasurementCache` | `Map<number, number>` | — | External column measurement cache |\n\n### `GridVirtualizerState`\n\n```ts\ninterface GridVirtualizerState {\n readonly cols: VirtualItem[];\n readonly rows: VirtualItem[];\n readonly totalHeight: number;\n readonly totalWidth: number;\n}\n```\n\n### `GridVirtualizer` — properties and methods\n\n**Read-only properties:** `rows`, `cols`, `scrollTop`, `scrollLeft`, `totalHeight`, `totalWidth`, `disposalSignal`, `disposed`\n\n| Method | Description |\n| ---------------------------------- | --------------------------------------------------------------------------------- |\n| `getSnapshot()` | Return the current `GridVirtualizerState` snapshot |\n| `subscribe(listener)` | Observe grid state changes; returns an unsubscribe function |\n| `update(next)` | Atomically update row/col counts, estimates, gaps, and overscan |\n| `measureRow(row, size)` | Record a row height |\n| `measureColumn(col, size)` | Record a column width |\n| `measureBatch(rows, cols)` | Measure rows and columns in a single coordinated rebuild pass |\n| `measureRowEl(row, el)` | Auto-measure row height via ResizeObserver. Returns disconnect fn |\n| `measureColEl(col, el)` | Auto-measure column width via ResizeObserver. Returns disconnect fn |\n| `refresh()` | Rebuild offset tables from current measurements |\n| `invalidate()` | Clear all measurements and rebuild from estimates |\n| `scrollToCell(row, col, options?)` | Scroll to bring a cell into view; no-op when `rowCount === 0` or `colCount === 0` |\n| `scrollToRow(row, options?)` | Scroll to bring a row into view; `rowAlign` controls alignment |\n| `scrollToColumn(col, options?)` | Scroll to bring a column into view; `colAlign` controls alignment |\n| `prependRows(n)` | Add `n` rows at the top; adjusts scroll offset to keep viewport stable |\n| `dispose()` | Teardown; idempotent |\n| `[Symbol.dispose]()` | Delegates to `dispose()` |\n\n`measureRowEl`/`measureColEl`'s `ResizeObserver` is also disconnected automatically on `dispose()` —\nthe returned disconnect function is only needed to stop observing a specific element early.\n\n### `ScrollToCellOptions`\n\n```ts\ninterface ScrollToCellOptions {\n behavior?: ScrollBehavior;\n colAlign?: 'auto' | 'center' | 'end' | 'start';\n rowAlign?: 'auto' | 'center' | 'end' | 'start';\n}\n```\n\n## Types\n\n### `ScrollStore<State>`\n\n```ts\ninterface ScrollStore<State> {\n getSnapshot: () => State;\n subscribe: (listener: () => void) => () => void;\n}\n```\n\nFramework-neutral external-store contract implemented by every Scroll controller. `getSnapshot()` returns the current state. `subscribe()` observes later state changes and returns an unsubscribe function.\n\n### `VirtualItem`\n\n```ts\ninterface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n```\n\n### `VirtualizerState`\n\n```ts\ninterface VirtualizerState {\n readonly items: VirtualItem[];\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n}\n```\n\n### `ScrollToIndexOptions`\n\n```ts\ninterface ScrollToIndexOptions {\n align?: 'auto' | 'center' | 'end' | 'start';\n behavior?: ScrollBehavior;\n /** Called when the scroll animation completes (instant scrolls: next microtask). */\n onComplete?: () => void;\n}\n```\n\n### `Overscan`\n\n```ts\ntype Overscan = number | { end?: number; start?: number };\n```\n\nPassing a number is shorthand for symmetric overscan on both sides.\n\n### `VirtualKey`\n\n```ts\ntype VirtualKey = number | string;\n```\n\n### `VirtualRenderItem<T>`\n\n```ts\ntype VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n```\n\n### `ScrollTarget`\n\n```ts\ntype ScrollTarget = HTMLElement | Window;\n```\n\n### `MeasurementCache`\n\n```ts\ntype MeasurementCache = Map<VirtualKey, number>;\n```\n\nUse `createMeasurementCache()` to create an empty cache:\n\n```ts\nimport { createMeasurementCache } from '@vielzeug/scroll';\n\nconst cache = createMeasurementCache();\nconst virt1 = createVirtualizer(el1, { count: 100, measurementCache: cache });\nconst virt2 = createVirtualizer(el2, { count: 100, measurementCache: cache });\n```\n\n### `RecycleFn`\n\n```ts\ntype RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n```\n\n### `VirtualizerUpdateOptions`\n\n```ts\ninterface VirtualizerUpdateOptions {\n autoMeasure?: boolean;\n count?: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: ((index: number) => VirtualKey) | undefined;\n /** Replace the active measurement cache. Existing entries are used immediately on the next rebuild. */\n measurementCache?: MeasurementCache;\n onChange?: ((state: VirtualizerState) => void) | undefined;\n onScrollEnd?: ((offset: number) => void) | undefined;\n onScrollingChange?: ((isScrolling: boolean) => void) | undefined;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sticky?: ((index: number) => boolean) | undefined;\n}\n```\n\n### `VirtualScrollerOptions<T>`\n\n`DomVirtualListOptions<T>` minus `listElement` and `scrollElement`, plus:\n\n```ts\ntype VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** CSS class applied to the generated scroll container element. */\n containerClass?: string;\n};\n```\n\n### `GridVirtualizerUpdateOptions`\n\n```ts\ninterface GridVirtualizerUpdateOptions {\n colCount?: number;\n colGap?: number;\n estimateColSize?: number | ((col: number) => number);\n estimateRowSize?: number | ((row: number) => number);\n onChange?: ((state: GridVirtualizerState) => void) | undefined;\n onRangeChange?: ((range: GridRangeChangeEvent) => void) | undefined;\n overscanX?: Overscan;\n overscanY?: Overscan;\n rowCount?: number;\n rowGap?: number;\n}\n```\n\n### `GridRangeChangeEvent`\n\nFired by `onRangeChange` on `createGridVirtualizer`. Zero-allocation alternative to `onChange` — no `rows`/`cols` arrays are allocated.\n\n```ts\ninterface GridRangeChangeEvent {\n firstCol: number;\n firstRow: number;\n lastCol: number;\n lastRow: number;\n}\n```\n\n### `VirtualizerOptions`\n\n```ts\ninterface VirtualizerOptions {\n autoMeasure?: boolean;\n count: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: (index: number) => VirtualKey;\n horizontal?: boolean;\n initialOffset?: number;\n measurementCache?: MeasurementCache;\n onChange?: (state: VirtualizerState) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sticky?: (index: number) => boolean;\n}\n```\n\n### `Virtualizer`\n\n```ts\ninterface Virtualizer extends ScrollStore<VirtualizerState> {\n readonly count: number;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n isAtEnd: (threshold?: number) => boolean;\n readonly isScrolling: boolean;\n readonly items: VirtualItem[];\n measure: (index: number, size: number) => void;\n measureBatch: (entries: Array<{ index: number; size: number }>) => void;\n measureEl: (index: number, el: HTMLElement) => () => void;\n prepend: (additionalCount: number) => void;\n refresh: () => void;\n remeasure: () => void;\n readonly scrollOffset: number;\n scrollToBottom: (options?: { behavior?: ScrollBehavior }) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n scrollToOffset: (offset: number, options?: { behavior?: ScrollBehavior }) => void;\n scrollToTop: (options?: { behavior?: ScrollBehavior }) => void;\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n update: (next: VirtualizerUpdateOptions) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n### `StickToBottomOptions`\n\n```ts\ntype StickToBottomOptions = {\n enabled?: boolean;\n threshold?: number;\n};\n```\n\n### `DomVirtualListOptions<T>`\n\n```ts\ntype DomVirtualListOptions<T> = {\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n listElement: HTMLElement;\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n stickToBottom?: boolean | StickToBottomOptions;\n sticky?: (index: number, item: T) => boolean;\n};\n```\n\n### `DomVirtualListController<T>`\n\n`Virtualizer` minus `prepend` and `update`, plus `setItems()`.\n\n```ts\ntype DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n```\n\n### `DomVirtualListRenderArgs<T>`\n\n```ts\ntype DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n```\n\n### `GroupSection<T>`\n\n```ts\ninterface GroupSection<T> {\n items: T[];\n label: string;\n}\n```\n\n### `GroupVirtualizerState<T>`\n\n```ts\ninterface GroupVirtualizerState<T> {\n readonly headers: GroupVirtualHeader[];\n readonly items: Array<GroupVirtualItem<T>>;\n readonly stickyHeader: GroupVirtualHeader | null;\n readonly totalSize: number;\n}\n```\n\n### `GroupVirtualItem<T>`\n\n```ts\ninterface GroupVirtualItem<T> extends VirtualItem {\n data: T;\n itemIndex: number;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualHeader`\n\n```ts\ninterface GroupVirtualHeader extends VirtualItem {\n label: string;\n sectionIndex: number;\n}\n```\n\n### `GroupVirtualizerOptions<T>`\n\n```ts\ninterface GroupVirtualizerOptions<T> {\n estimateHeaderSize?: number | ((section: GroupSection<T>, sectionIndex: number) => number);\n estimateItemSize?: number | ((item: T, itemIndex: number, sectionIndex: number) => number);\n getItemKey?: (item: T, itemIndex: number, sectionIndex: number) => VirtualKey;\n horizontal?: boolean;\n measurementCache?: MeasurementCache;\n onChange?: (state: GroupVirtualizerState<T>) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n sections: Array<GroupSection<T>>;\n}\n```\n\n### `GroupVirtualizerUpdateOptions<T>`\n\n```ts\ninterface GroupVirtualizerUpdateOptions<T> {\n estimateHeaderSize?: number | ((section: GroupSection<T>, sectionIndex: number) => number);\n estimateItemSize?: number | ((item: T, itemIndex: number, sectionIndex: number) => number);\n getItemKey?: (item: T, itemIndex: number, sectionIndex: number) => VirtualKey;\n measurementCache?: MeasurementCache;\n onChange?: ((state: GroupVirtualizerState<T>) => void) | undefined;\n onScrollEnd?: ((offset: number) => void) | undefined;\n onScrollingChange?: ((isScrolling: boolean) => void) | undefined;\n overscan?: Overscan;\n scrollEndDelay?: number;\n}\n```\n\n### `GroupVirtualizer<T>`\n\n```ts\ninterface GroupVirtualizer<T> extends ScrollStore<GroupVirtualizerState<T>> {\n readonly count: number;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n readonly isScrolling: boolean;\n readonly items: ReadonlyArray<GroupVirtualItem<T>>;\n measure: (index: number, size: number) => void;\n measureBatch: (entries: Array<{ index: number; size: number }>) => void;\n measureEl: (index: number, el: HTMLElement) => () => void;\n refresh: () => void;\n readonly scrollOffset: number;\n scrollToBottom: (options?: { behavior?: ScrollBehavior }) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n scrollToItem: (sectionIndex: number, itemIndex: number, options?: ScrollToIndexOptions) => void;\n scrollToOffset: (offset: number, options?: { behavior?: ScrollBehavior }) => void;\n scrollToSection: (sectionIndex: number, options?: ScrollToIndexOptions) => void;\n scrollToTop: (options?: { behavior?: ScrollBehavior }) => void;\n readonly stickyItems: VirtualItem[];\n readonly totalSize: number;\n update: (sections: Array<GroupSection<T>>, opts?: GroupVirtualizerUpdateOptions<T>) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n### `GridVirtualizerState`\n\n```ts\ninterface GridVirtualizerState {\n readonly cols: VirtualItem[];\n readonly rows: VirtualItem[];\n readonly totalHeight: number;\n readonly totalWidth: number;\n}\n```\n\n### `ScrollToCellOptions`\n\n```ts\ninterface ScrollToCellOptions {\n behavior?: ScrollBehavior;\n colAlign?: 'auto' | 'center' | 'end' | 'start';\n rowAlign?: 'auto' | 'center' | 'end' | 'start';\n}\n```\n\n### `GridVirtualizerOptions`\n\n```ts\ninterface GridVirtualizerOptions {\n colCount: number;\n colGap?: number;\n colMeasurementCache?: Map<number, number>;\n estimateColSize?: number | ((col: number) => number);\n estimateRowSize?: number | ((row: number) => number);\n initialScrollLeft?: number;\n initialScrollTop?: number;\n onChange?: (state: GridVirtualizerState) => void;\n onRangeChange?: (range: GridRangeChangeEvent) => void;\n overscanX?: Overscan;\n overscanY?: Overscan;\n rowCount: number;\n rowGap?: number;\n rowMeasurementCache?: Map<number, number>;\n}\n```\n\n### `GridVirtualizer`\n\n```ts\ninterface GridVirtualizer extends ScrollStore<GridVirtualizerState> {\n readonly cols: VirtualItem[];\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n invalidate: () => void;\n measureBatch: (rows: Array<{ index: number; size: number }>, cols: Array<{ index: number; size: number }>) => void;\n measureColEl: (col: number, el: HTMLElement) => () => void;\n measureColumn: (col: number, size: number) => void;\n measureRow: (row: number, size: number) => void;\n measureRowEl: (row: number, el: HTMLElement) => () => void;\n prependRows: (additionalRowCount: number) => void;\n refresh: () => void;\n readonly rows: VirtualItem[];\n readonly scrollLeft: number;\n scrollToCell: (row: number, col: number, options?: ScrollToCellOptions) => void;\n scrollToColumn: (col: number, options?: Pick<ScrollToCellOptions, 'behavior' | 'colAlign'>) => void;\n readonly scrollTop: number;\n scrollToRow: (row: number, options?: Pick<ScrollToCellOptions, 'behavior' | 'rowAlign'>) => void;\n readonly totalHeight: number;\n readonly totalWidth: number;\n update: (next: GridVirtualizerUpdateOptions) => void;\n [Symbol.dispose]: () => void;\n}\n```\n\n## Errors\n\n| Class | Thrown when | Notable properties |\n| --- | --- | --- |\n| `ScrollError` | Base class for every Scroll error. | Use `instanceof ScrollError` to narrow unknown errors narrows errors from this package. |\n| `ScrollConfigError` | A constructor or `update()` receives invalid static configuration. | Extends `ScrollError`; malformed JavaScript values also use this class. |\n| `ScrollRangeError` | A DOM virtual-list render detects that a caller mutated its items array without calling `setItems()` again. | Extends `ScrollError`; message includes stale index and current item count. |\n\nRuntime estimator failures, stale measurements, and out-of-range navigation remain resilient: they fall back, no-op, or clamp as documented.\n\n### Constants\n\n```ts\nconst DEFAULT_ESTIMATE_SIZE = 36; // default estimateSize\nconst DEFAULT_OVERSCAN = 3; // default overscan on each side\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Scroll — Usage Guide\ndescription: Fixed and variable heights, measurement, programmatic scrolling, and framework integration for Scroll.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRender only visible rows by passing a scroll container, a total item count, and a size estimate. Scroll calls `onChange` with the visible window whenever it changes.\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst scrollEl = document.querySelector<HTMLElement>('.scroll-container')!;\nconst listEl = document.querySelector<HTMLElement>('.list')!;\n\nconst virt = createVirtualizer(scrollEl, {\n count: 10_000,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = `Row ${item.index}`;\n listEl.appendChild(el);\n }\n },\n});\n\n// Cleanup\nvirt.dispose();\n```\n\n```html\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;position:relative;\">\n <div class=\"list\" style=\"position:relative;\"></div>\n</div>\n```\n\n## DOM Layout Requirements\n\nScroll uses **absolute positioning** for rendered items inside a relative container that stretches to the full list height. Your HTML needs three elements:\n\n```html\n<!-- 1. Scroll container — has a fixed height and overflow:auto/scroll -->\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;position:relative;\">\n <!-- 2. Spacer — height set to totalSize so the scrollbar is correct -->\n <div class=\"spacer\" style=\"position:relative;\">\n <!-- 3. Item container — items positioned absolutely inside here -->\n <div class=\"items\"></div>\n </div>\n</div>\n```\n\nA common alternative is to make the spacer and item container the same element:\n\n```html\n<div class=\"scroll-container\" style=\"height:400px;overflow:auto;\">\n <!-- Single relative container; items are absolute children -->\n <div class=\"list\" style=\"position:relative;\"></div>\n</div>\n```\n\n## DOM Adapter for Dropdowns and Listboxes\n\nIf your component already has a dropdown scroll container and a listbox element, use `createDomVirtualList`. It wraps the `Virtualizer` lifecycle and keeps the integration surface small. Items arrive as `VirtualRenderItem<T>` — a `VirtualItem` enriched with a `.data` field. Use `recycle` for efficient DOM node reuse.\n\nThe virtualizer is created lazily on the first non-empty `setItems()` call and destroyed automatically when `setItems([])` is called (clearing list styles in the process).\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\ntype Option = { disabled?: boolean; label: string; value: string };\n\nlet options: Option[] = [];\n\nconst domVirtualList = createDomVirtualList<Option>({\n estimateSize: 36,\n gap: 6,\n getItemKey: (_index, option) => option.value,\n listElement: listboxEl,\n overscan: { end: 4, start: 4 },\n render: ({ items, listEl, recycle }) => {\n for (const item of items) {\n const row = recycle(item.data.value, () => document.createElement('button'));\n row.type = 'button';\n row.className = 'option';\n row.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.start}px);height:${item.size}px;`;\n row.textContent = item.data.label;\n row.disabled = !!item.data.disabled;\n listEl.appendChild(row);\n }\n },\n scrollElement: dropdownEl,\n});\n\n// Keep in sync when options change\ndomVirtualList.setItems(options);\n\n// Open: setItems populates the list\n// Close: setItems([]) destroys the virtualizer and clears list styles\ndomVirtualList.setItems(isOpen ? options : []);\n\n// Keyboard nav\ndomVirtualList.scrollToIndex(focusedIndex, { align: 'auto' });\n\n// Component teardown\ndomVirtualList.dispose();\n```\n\nFor variable-height rows, pass `getItemKey` so measurements survive `setItems()` calls when items reorder or are filtered.\n\nWhen multiple sizes are available at once, use `measureBatch` to coalesce into a single rebuild:\n\n```ts\ndomVirtualList.measureBatch(\n entries.map((e) => ({ index: Number(e.target.dataset.index), size: e.contentRect.height })),\n);\n```\n\nUse `domVirtualList.invalidate()` to discard all cached measurements.\n\n## Fixed Heights\n\nPass a single number to `estimateSize` when all rows are the same height. This is the simplest and most performant case — the offset table never needs to be rebuilt during scrolling.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: 10_000,\n estimateSize: 36, // every row is 36px\n onChange: ({ items, totalSize }) => {\n list.style.height = `${totalSize}px`;\n list.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = data[item.index].name;\n list.appendChild(el);\n }\n },\n});\n```\n\n## Variable Heights — Estimator\n\nPass a **per-index function** to `estimateSize` when rows have predictable but non-uniform heights (e.g. group headers vs. regular rows). The offset table is built once at attach time using these estimates.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: flatList.length,\n estimateSize: (i) => (flatList[i].type === 'header' ? 48 : 36),\n onChange: ({ items, totalSize }) => {\n // render...\n },\n});\n```\n\n## Variable Heights — Measured\n\nFor truly dynamic heights (e.g. text wrapping, embedded images), render items at their estimated size first, then report the actual measured height with `measure()`. Scroll will coalesce all measurement calls within a single microtask tick into one offset rebuild.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 60, // initial estimate\n onChange: ({ items, totalSize }) => {\n list.style.height = `${totalSize}px`;\n list.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.dataset.index = String(item.index);\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;`;\n el.textContent = rows[item.index].body;\n list.appendChild(el);\n }\n\n // Measure after the DOM has painted\n requestAnimationFrame(() => {\n for (const item of items) {\n const el = list.querySelector<HTMLElement>(`[data-index=\"${item.index}\"]`);\n if (el) virt.measure(item.index, el.offsetHeight);\n }\n });\n },\n});\n```\n\n::: tip Measurement is idempotent\n`measure(index, height)` is a no-op when the new height matches the current effective height (measured or estimated). It is safe to call on every render without triggering unnecessary rebuilds.\n:::\n\n## Variable Heights — Batch Measurement\n\nWhen a `ResizeObserver` fires with multiple entries at once, use `measureBatch()` to apply all sizes in a single offset rebuild instead of triggering one rebuild per `measure()` call.\n\n```ts\nconst observer = new ResizeObserver((entries) => {\n virt.measureBatch(\n entries\n .filter((e) => e.target instanceof HTMLElement && e.target.dataset.index)\n .map((e) => ({\n index: Number((e.target as HTMLElement).dataset.index),\n size: e.contentRect.height,\n })),\n );\n});\n\n// Observe each rendered row\nfor (const item of virt.items) {\n const el = listEl.querySelector<HTMLElement>(`[data-index=\"${item.index}\"]`);\n if (el) observer.observe(el);\n}\n```\n\n## Overscan\n\n`overscan` controls how many extra items render outside the visible viewport on each side. Higher values reduce the chance of blank rows during fast scrolling; lower values keep the DOM smaller.\n\n```ts\ncreateVirtualizer(scrollEl, {\n count: 1_000,\n estimateSize: 36,\n overscan: 5, // symmetric shorthand — same as { start: 5, end: 5 } (default: 3)\n onChange: () => {\n /* ... */\n },\n});\n```\n\nAsymmetric overscan:\n\n```ts\ncreateVirtualizer(scrollEl, {\n count: 1_000,\n estimateSize: 36,\n overscan: { start: 8, end: 2 },\n onChange: () => {\n /* ... */\n },\n});\n```\n\n## Horizontal Lists\n\nSet `horizontal: true` to virtualize along the X axis.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: chips.length,\n estimateSize: 120,\n horizontal: true,\n onChange: ({ items, totalSize }) => {\n list.style.width = `${totalSize}px`;\n\n for (const item of items) {\n const chip = document.createElement('button');\n chip.style.cssText = `position:absolute;left:${item.start}px;top:0;width:${item.size}px;`;\n chip.textContent = chips[item.index].label;\n list.appendChild(chip);\n }\n },\n});\n```\n\n## Window Scroll Target\n\n`createVirtualizer` accepts `window` as the scroll target.\n\n```ts\nconst virt = createVirtualizer(window, {\n count: rows.length,\n estimateSize: 40,\n initialOffset: 320,\n onChange: ({ items, totalSize }) => {\n spacer.style.height = `${totalSize}px`;\n renderRows(items);\n },\n});\n```\n\n## Scroll State\n\nUse `virt.scrollOffset` to read the current scroll position at any time.\n\n```ts\nconst virt = createVirtualizer(scrollEl, { count: rows.length, estimateSize: 36, onChange: render });\n\n// Accessed outside onChange\nconsole.log(virt.scrollOffset);\n```\n\n## Updating Options\n\nWhen data or render strategy changes, call `update()` with one or more option fields. Updates apply atomically and trigger re-render when needed. Counts, gaps, and overscan must be finite non-negative integers; numeric size estimates must be finite positive values; offsets and `scrollEndDelay` must be finite non-negative numbers. Invalid constructor or `update()` values throw `ScrollConfigError` before any change applies.\n\nRuntime layout data stays resilient: estimator callbacks that throw or return invalid sizes fall back to the default estimate, stale measurements are ignored, and out-of-range navigation clamps or no-ops.\n\n```ts\n// Load more data\ndata.push(...newItems);\nvirt.update({ count: data.length });\n```\n\n```ts\n// Change multiple options together\nvirt.update({ count: data.length, overscan: { start: 5, end: 5 } });\n\n// Rebuild after reordering/filtering stable-key rows\nvirt.refresh();\n```\n\n## Switching Row Density\n\nUpdating `estimateSize` clears all previously measured heights, rebuilds offsets, and re-renders. This makes density switching (compact / comfortable / spacious views) straightforward.\n\n```ts\nfunction setDensity(mode: 'compact' | 'comfortable') {\n virt.update({ estimateSize: mode === 'compact' ? 32 : 48 });\n}\n```\n\n## Programmatic Scrolling\n\n### `scrollToIndex(index, options?)`\n\nScroll to bring a specific item into view.\n\n| `align` | Behaviour |\n| ------------------ | ------------------------------------------------------------------------ |\n| `'start'` | Item top aligns with the container top |\n| `'end'` | Item bottom aligns with the container bottom |\n| `'center'` | Item is centered in the viewport |\n| `'auto'` (default) | No scroll if already fully visible; otherwise scrolls the minimum amount |\n\n```ts\n// Jump to item 500 at the top of the viewport\nvirt.scrollToIndex(500, { align: 'start' });\n\n// Smooth-scroll to an item, centering it\nvirt.scrollToIndex(500, { align: 'center', behavior: 'smooth' });\n\n// Scroll only if the item is not already visible\nvirt.scrollToIndex(focusedIndex, { align: 'auto' });\n```\n\nOut-of-range indices are clamped silently: negative values scroll to item `0`, values ≥ `count` scroll to the last item.\n\n### `scrollToOffset(offset, options?)`\n\nScroll to an exact pixel position, useful for restoring a previously saved scroll state.\n\n```ts\n// Restore scroll position\nconst savedOffset = sessionStorage.getItem('scrollOffset');\nif (savedOffset) virt.scrollToOffset(Number(savedOffset));\n\n// Save on scroll\nscrollEl.addEventListener('scroll', () => {\n sessionStorage.setItem('scrollOffset', String(scrollEl.scrollTop));\n});\n```\n\n### `scrollToTop(options?)` / `scrollToBottom(options?)`\n\nConvenience wrappers to jump directly to the start or end of the list.\n\n```ts\n// Jump to the top\nvirt.scrollToTop();\n\n// Jump to the bottom with smooth scroll\nvirt.scrollToBottom({ behavior: 'smooth' });\n```\n\n### Chat \"stick to bottom on new message\"\n\n`createDomVirtualList`'s `stickToBottom` option automates the common chat/log pattern: follow new messages while the user is at the bottom, but never yank them away from history they scrolled up to read.\n\n```ts\nimport { createDomVirtualList } from '@vielzeug/scroll';\n\nconst chat = createDomVirtualList<Message>({\n estimateSize: 48,\n getItemKey: (_, m) => m.id,\n listElement: listEl,\n render: renderMessages,\n scrollElement: scrollEl,\n stickToBottom: true, // or { threshold: 80 } to widen the \"still at bottom\" tolerance\n});\n\nchat.setItems(messages);\n\n// New message arrives — follows only if the user hasn't scrolled up.\nsocket.on('message', (msg) => {\n messages = [...messages, msg];\n chat.setItems(messages);\n});\n```\n\nIt also follows a **streaming** last message that grows in place (tokens appended to the same message object, array length unchanged) — every `setItems()` call re-checks \"was the list at the end before this update?\", not just count changes. Build `isAtEnd()` from `createVirtualizer` directly for custom cases (e.g. showing a \"jump to latest\" button only while scrolled away):\n\n```ts\nconst showJumpButton = !virt.isAtEnd();\n```\n\n## Infinite Scroll — Loading More at the End\n\nUse `isAtEnd(threshold)` to fetch the next page as the user nears the bottom. `isAtEnd()` reports scroll position only — it keeps returning `true` while a fetch is in flight — so guard it with your own `loading` flag to avoid firing the same request twice.\n\n```ts\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\nlet rows = await fetchPage(0);\nlet loading = false;\n\nlet virt: Virtualizer;\nvirt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n\n if (!loading && virt.isAtEnd(200)) {\n loading = true;\n fetchPage(rows.length).then((nextRows) => {\n rows = [...rows, ...nextRows];\n virt.update({ count: rows.length });\n loading = false;\n });\n }\n },\n});\n```\n\n`isAtEnd(200)` fires once the viewport is within 200px of the bottom — tune the threshold to your row height and fetch latency. `loading` is the only guard needed: it's cleared once the new page lands, and `update({ count })` re-triggers `onChange`, which re-checks `isAtEnd()` against the new total on the next scroll.\n\n## Shared Measurement Cache\n\nWhen the same items are displayed across multiple virtualizer instances (e.g. a list and a detail panel that share row heights), pass a shared `MeasurementCache` created by `createMeasurementCache()`. Measurements recorded by one virtualizer are immediately available to all others using the same cache.\n\n```ts\nimport { createMeasurementCache, createVirtualizer } from '@vielzeug/scroll';\n\nconst cache = createMeasurementCache();\n\nconst listVirt = createVirtualizer(listScrollEl, {\n count: rows.length,\n estimateSize: 36,\n measurementCache: cache,\n onChange: renderList,\n});\n\nconst previewVirt = createVirtualizer(previewScrollEl, {\n count: rows.length,\n estimateSize: 36,\n measurementCache: cache,\n onChange: renderPreview,\n});\n\n// A measurement on listVirt is reflected in previewVirt immediately.\nlistVirt.measure(0, 72);\n```\n\nThe cache is a plain `Map<VirtualKey, number>` — you can pre-populate it from server data or persist it across sessions.\n\n```ts\n// Pre-populate from server-sent sizes\nconst cache = createMeasurementCache();\nfor (const { id, height } of serverSizes) cache.set(id, height);\n```\n\n## Invalidating Measurements\n\nCall `invalidate()` after an event that changes item heights without a data change — for example, a font load, a viewport width change that causes text to reflow, or toggling between a grid and list layout.\n\n```ts\ndocument.fonts.ready.then(() => virt.invalidate());\n```\n\nOn variable-height lists, `scrollToIndex()` uses the current estimate/measured cache. If you need an exact post-layout position after heights change, call `invalidate()` before scrolling again.\n\nFor same-length updates, call `setItems()` (DOM adapter) or `update()` (core). If the rendered height of rows changed, call `invalidate()` before scrolling again.\n\n## Lifecycle — create and dispose\n\n`createVirtualizer(el, options)` attaches immediately to the provided scroll container. If your container is replaced, dispose the old instance and create a new one.\n\n```ts\nlet virt = createVirtualizer(scrollContainerEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: render,\n});\n\nfunction remount(nextScrollContainerEl: HTMLElement) {\n virt.dispose();\n virt = createVirtualizer(nextScrollContainerEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: render,\n });\n}\n```\n\n`dispose()` is idempotent and safe to call multiple times.\n\n### Explicit Resource Management\n\n```ts\n// The `using` keyword calls virt.dispose() automatically at block exit\n{\n using virt = createVirtualizer(scrollEl, { count: rows.length, onChange: render });\n // ... use virt ...\n} // → virt.dispose() called here\n```\n\n## Keyboard Navigation\n\nScroll ships no built-in key handler — keyboard scrolling is navigation policy, and the virtualizer already exposes everything it needs: `scrollToIndex()`. Compose it with your own focus tracking, or with `matchKey` from `@vielzeug/keymap` for one key vocabulary across the app:\n\n```ts\nimport { createVirtualizer } from '@vielzeug/scroll';\nimport { matchKey } from '@vielzeug/keymap';\n\nlet focusedIndex = 0;\n\nscrollEl.addEventListener('keydown', (event) => {\n if (matchKey(event, 'ArrowDown')) focusedIndex = Math.min(focusedIndex + 1, count - 1);\n else if (matchKey(event, 'ArrowUp')) focusedIndex = Math.max(focusedIndex - 1, 0);\n else if (matchKey(event, 'Home')) focusedIndex = 0;\n else if (matchKey(event, 'End')) focusedIndex = count - 1;\n else return;\n\n event.preventDefault();\n virt.scrollToIndex(focusedIndex, { align: 'auto' });\n});\n```\n\n**Requirements:**\n- The scroll container (or a descendant) must have keyboard focus for events to fire — give it `tabindex=\"0\"`\n- `align: 'auto'` scrolls only when the item is outside the visible area\n\nSee the [Keyboard Navigation example](./examples/keyboard-navigation.md) for the full pattern with focus highlighting.\n\n## Auto-Measurement\n\nEnable automatic item measurement for dynamic or user-generated content that changes size. When `autoMeasure` is enabled, the virtualizer measures visible items via `ResizeObserver` and updates layout in real time.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: messages.length,\n estimateSize: 36, // Initial guess; will be measured\n autoMeasure: true, // Automatically measure visible items\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const el = document.createElement('div');\n // IMPORTANT: Set data-vz-key for auto-measure to find the element\n el.setAttribute('data-vz-key', String(item.index));\n el.textContent = messages[item.index]?.text ?? '';\n listEl.appendChild(el);\n }\n },\n});\n```\n\n**Requirements:**\n- Every rendered item must have a `data-vz-key` attribute with a unique value\n- Must use a DOM scroll target (not `Window`)\n- Elements must be in the DOM by the time `ResizeObserver` fires (usually the next microtask)\n\n**Use cases:**\n- Chat lists where messages expand on load\n- Expandable sections with collapsing text\n- Lazy-loaded thumbnails that arrive with unknown heights\n- User-resizable rows or dynamic content (videos, iframes)\n\n**Performance notes:**\n- Auto-measurement queries the DOM every render cycle — avoid with very large visible windows (100+ items)\n- For finer control, use the manual `measureEl()` method instead\n- Enable only on lists with truly variable-height items\n\n## Reactive Integration\n\nEvery Scroll controller is a framework-neutral external store. Read its current state with `getSnapshot()` and observe future changes with `subscribe()`.\n\n```ts\nconst virt = createVirtualizer(scrollEl, {\n count: 1000,\n estimateSize: 36,\n});\n\nconst unsubscribe = virt.subscribe(() => {\n const { items, totalSize } = virt.getSnapshot();\n console.log(`Visible: ${items.length} items, total height: ${totalSize}px`);\n});\n\nunsubscribe();\nvirt.dispose();\n```\n\nBridge the same controller into Ripple without adding Ripple as a Scroll dependency:\n\n```ts\nimport { effect, fromSubscribable } from '@vielzeug/ripple';\nimport { createVirtualizer } from '@vielzeug/scroll';\n\nconst virt = createVirtualizer(scrollEl, {\n count: 1000,\n estimateSize: 36,\n});\nconst scrollState = fromSubscribable(virt, { signal: virt.disposalSignal });\nconst renderEffect = effect(() => {\n const { items, totalSize } = scrollState.value;\n console.log(`Visible: ${items.length} items, total height: ${totalSize}px`);\n});\n\nrenderEffect.dispose();\nvirt.dispose();\n```\n\nThe same contract works with `createDomVirtualList()`, `createVirtualScroller()`, `createGroupedVirtualizer()`, and `createGridVirtualizer()`.\n\n## Framework Integration\n\nScroll is rendering-layer agnostic. The pattern is always the same: create the virtualizer when your scroll container is mounted, re-render your DOM in `onChange`, and call `dispose()` on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\nimport { useEffect, useLayoutEffect, useRef } from 'react';\n\ninterface Row {\n id: number;\n label: string;\n}\n\nfunction VirtualList({ rows }: { rows: Row[] }) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const virtRef = useRef<Virtualizer | null>(null);\n\n useEffect(() => {\n const scrollEl = scrollRef.current;\n const listEl = listRef.current;\n if (!scrollEl || !listEl) return;\n\n const virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n virtRef.current = virt;\n return () => virt.dispose();\n }, []); // attach once\n\n // useLayoutEffect, not useEffect: syncs count before paint. With useEffect,\n // the DOM (and anything reading `rows`) paints once with the new length before\n // the virtualizer's internal count catches up, which can render stale/out-of-bounds indices.\n useLayoutEffect(() => {\n virtRef.current?.update({ count: rows.length });\n }, [rows.length]);\n\n return (\n <div ref={scrollRef} style={{ height: 400, overflow: 'auto', position: 'relative' }}>\n <div ref={listRef} style={{ position: 'relative' }} />\n </div>\n );\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{ rows: { id: number; label: string }[] }>();\nconst scrollRef = ref<HTMLElement | null>(null);\nconst listRef = ref<HTMLElement | null>(null);\nlet virt: Virtualizer | null = null;\n\nonMounted(() => {\n if (!scrollRef.value || !listRef.value) return;\n const listEl = listRef.value;\n virt = createVirtualizer(scrollRef.value, {\n count: props.rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = props.rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n});\nwatch(\n () => props.rows.length,\n (n) => {\n virt?.update({ count: n });\n },\n);\nonUnmounted(() => virt?.dispose());\n</script>\n\n<template>\n <div ref=\"scrollRef\" style=\"height:400px;overflow:auto;position:relative;\">\n <div ref=\"listRef\" style=\"position:relative;\" />\n </div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\n let { rows }: { rows: { id: number; label: string }[] } = $props();\n let scrollEl: HTMLElement;\n let listEl: HTMLElement;\n let virt: Virtualizer;\n\n $effect(() => {\n virt = createVirtualizer(scrollEl, {\n count: rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n return () => virt.dispose();\n });\n\n $effect(() => { virt?.update({ count: rows.length }); });\n</script>\n\n<div bind:this={scrollEl} style=\"height:400px;overflow:auto;position:relative;\">\n <div bind:this={listEl} style=\"position:relative;\" />\n</div>\n```\n\n```ts [Web Components]\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { createVirtualizer, type Virtualizer } from '@vielzeug/scroll';\n\n@customElement('virtual-list')\nclass VirtualList extends LitElement {\n static styles = css`\n .scroll {\n height: 400px;\n overflow: auto;\n position: relative;\n }\n .list {\n position: relative;\n }\n `;\n\n @property({ type: Array }) rows: { label: string }[] = [];\n #virt: Virtualizer | null = null;\n\n firstUpdated() {\n const scrollEl = this.renderRoot.querySelector<HTMLElement>('.scroll')!;\n const listEl = this.renderRoot.querySelector<HTMLElement>('.list')!;\n this.#virt = createVirtualizer(scrollEl, {\n count: this.rows.length,\n estimateSize: 36,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n for (const item of items) {\n const el = document.createElement('div');\n el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:36px;`;\n el.textContent = this.rows[item.index]?.label ?? '';\n listEl.appendChild(el);\n }\n },\n });\n }\n\n updated() {\n this.#virt?.update({ count: this.rows.length });\n }\n disconnectedCallback() {\n this.#virt?.dispose();\n super.disconnectedCallback();\n }\n render() {\n return html`<div class=\"scroll\"><div class=\"list\"></div></div>`;\n }\n}\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Putting `rows` in the `useEffect` dependency array causes the virtualizer to be destroyed and recreated on every data update. Only include the scroll element reference. Call `virt.update({ count })` from a separate `useEffect` for data changes.\n- **React:** Use `useLayoutEffect`, not `useEffect`, for the `count`-sync effect. `useEffect` fires after paint — a new `count` can reach the DOM (e.g. via other state derived from `rows`) before `update({ count })` runs, rendering stale or out-of-bounds indices for one frame.\n- **Vue 3:** `ref.value` is `null` inside `setup()` — the DOM doesn't exist yet. Always create the virtualizer inside `onMounted`, not in `setup()`.\n- **Svelte:** In Svelte 5, `$effect` with `bind:this` runs after the DOM is painted. The `bind:this` variable is available when the `$effect` runs — no extra tick needed.\n- **Web Components:** `firstUpdated` fires once after the first render. Use `updated()` for subsequent prop changes — Lit calls it every time `rows` changes.\n\n## Working with Other Vielzeug Libraries\n\n### With Ore\n\nBuild a virtualizing custom element using Ore for the component shell and Scroll for the rendering engine.\n\n```ts\nimport { define, html, onMounted, ref } from '@vielzeug/ore';\nimport { createVirtualizer } from '@vielzeug/scroll';\n\ndefine('virtual-list', {\n setup() {\n const scrollRef = ref<HTMLElement>();\n const listRef = ref<HTMLElement>();\n\n onMounted(() => {\n if (!scrollRef.value || !listRef.value) return;\n const listEl = listRef.value;\n const virt = createVirtualizer(scrollRef.value, {\n count: 1000,\n estimateSize: 40,\n onChange: ({ items, totalSize }) => {\n listEl.style.height = `${totalSize}px`;\n listEl.replaceChildren();\n\n for (const item of items) {\n const row = document.createElement('div');\n\n row.style.cssText = `position:absolute;top:${item.start}px;height:40px;`;\n row.textContent = `Row ${item.index}`;\n listEl.appendChild(row);\n }\n },\n },\n },\n });\n return () => virt.dispose();\n });\n\n return () => html`\n <div ref=${scrollRef} style=\"height:400px;overflow:auto;position:relative\">\n <div ref=${listRef} style=\"position:relative\"></div>\n </div>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Always provide `count` and `estimateSize` as a starting point, even for variable-height lists — measurements refine the estimates.\n- Call `dispose()` in the framework cleanup callback (useEffect return, onUnmounted, onDestroy) to free resize observers.\n- Use `overscan` to pre-render rows above and below the visible area to reduce blank flicker during fast scrolling.\n- Prefer `scrollToIndex()` with `align: 'start'` for programmatic navigation; use `align: 'center'` for focus management.\n- Use `createDomVirtualList()` for comboboxes, listboxes, and selects — it manages the virtualizer lifecycle and DOM node pooling for you.\n- Invalidate measurements with `invalidate()` when item content changes size (e.g., after expanding an accordion row).\n- For very large lists (>100k items), set a narrower `overscan` to limit DOM node count at any one time.\n- Use `refresh()` when item data or sizes may have changed; it rebuilds the offset table and re-emits.\n",
|
|
7
7
|
"examples": "---\ntitle: Scroll — Examples\ndescription: Practical examples and recipes for scroll.\n---\n\n## Examples\n\n- [Basic Fixed Height List](./examples/basic-fixed-height-list.md)\n- [Variable Height With Measurement](./examples/variable-height-with-measurement.md)\n- [Grouped List Headers Plus Rows](./examples/grouped-list-headers-plus-rows.md)\n- [Infinite Scroll Load More](./examples/infinite-scroll-load-more.md)\n- [Keyboard Navigation](./examples/keyboard-navigation.md)\n- [Restore Scroll Position](./examples/restore-scroll-position.md)\n- [Density Toggle Compact Comfortable](./examples/density-toggle-compact-comfortable.md)\n- [DOM Virtual List Combobox Pattern](./examples/dom-virtual-list-combobox-pattern.md)\n- [Grid Virtualizer](./examples/grid-virtualizer.md)\n- [Reactive Virtualizer](./examples/reactive-virtualizer.md)\n- [Infinite Scroll with Analytics and Prefetch](./examples/on-range-change.md)\n- [Sticky Items in DOM Virtual List](./examples/dom-virtual-list-sticky.md)\n- [Recreate on Remount](./examples/using-virtualizer-directly-without-createvirtualizer.md)\n- [Explicit Resource Management (`using`)](./examples/explicit-resource-management-using.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"VirtualScrollerOptions": "export type {\n DomVirtualListController,\n DomVirtualListOptions,\n DomVirtualListRenderArgs,\n RecycleFn,\n StickToBottomOptions,\n VirtualRenderItem,\n VirtualScrollerOptions,\n} from './dom-virtual-list';",
|
|
74
74
|
"createDomVirtualList": "export { createDomVirtualList, createVirtualScroller } from './dom-virtual-list';",
|
|
75
75
|
"createVirtualScroller": "export { createDomVirtualList, createVirtualScroller } from './dom-virtual-list';",
|
|
76
|
-
"
|
|
77
|
-
"ScrollError": "export {
|
|
78
|
-
"ScrollRangeError": "export {
|
|
76
|
+
"ScrollConfigError": "export { ScrollConfigError, ScrollError, ScrollRangeError } from './errors';",
|
|
77
|
+
"ScrollError": "export { ScrollConfigError, ScrollError, ScrollRangeError } from './errors';",
|
|
78
|
+
"ScrollRangeError": "export { ScrollConfigError, ScrollError, ScrollRangeError } from './errors';",
|
|
79
79
|
"GridRangeChangeEvent": "export type {\n GridRangeChangeEvent,\n GridVirtualizer,\n GridVirtualizerOptions,\n GridVirtualizerState,\n GridVirtualizerUpdateOptions,\n ScrollToCellOptions,\n} from './grid-virtualizer';",
|
|
80
80
|
"GridVirtualizer": "export type {\n GridRangeChangeEvent,\n GridVirtualizer,\n GridVirtualizerOptions,\n GridVirtualizerState,\n GridVirtualizerUpdateOptions,\n ScrollToCellOptions,\n} from './grid-virtualizer';",
|
|
81
81
|
"GridVirtualizerOptions": "export type {\n GridRangeChangeEvent,\n GridVirtualizer,\n GridVirtualizerOptions,\n GridVirtualizerState,\n GridVirtualizerUpdateOptions,\n ScrollToCellOptions,\n} from './grid-virtualizer';",
|