@domphy/virtual
v0.18.3
Published
Domphy Virtual - headless list/grid virtualization, a 1-1 port of @tanstack/virtual-core
Maintainers
Readme
@domphy/virtual
domphy.com · Docs · npm
Headless list, grid, and window virtualization for Domphy apps: render only the rows/columns in view, with dynamic measurement, sticky ranges, and smooth scroll-to.
Like the rest of Domphy, the core is framework-agnostic with zero dependencies. The Domphy adapter lives in src/domphy/.
Install
npm install @domphy/virtual @domphy/core@domphy/core is a peer dependency of the adapter only; the main entry is dependency-free.
Quick Example
import { createVirtualizer } from "@domphy/virtual/domphy"
import type { DomphyElement } from "@domphy/core"
const rows = Array.from({ length: 10000 }, (_, i) => `Row ${i}`)
const list = createVirtualizer<HTMLDivElement, HTMLDivElement>({
count: rows.length,
estimateSize: () => 32,
overscan: 8,
})
const App: DomphyElement<"div"> = {
// scroll container
div: [
{
// total-size spacer; virtual items are absolutely positioned inside
div: (l) =>
list.getVirtualItems(l).map((item) => ({
div: rows[item.index],
style: {
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
},
_key: item.key,
"data-index": item.index,
_onMount: (node) => list.measureElement(node.domElement),
_onRemove: () => list.measureElement(null),
})),
style: {
position: "relative",
height: (l) => `${list.getTotalSize(l)}px`,
width: "100%",
},
},
],
style: { height: "400px", overflow: "auto" },
_onMount: (node) => list.setScrollElement(node.domElement as HTMLDivElement),
_onRemove: () => list.destroy(),
}Adapter API
createVirtualizer(options) returns a handle:
| Member | Description |
| --- | --- |
| getVirtualItems(l) | Reactive list of visible VirtualItems — read with the listener inside the items function. |
| getTotalSize(l) | Reactive total scroll size, for the spacer's height/width. |
| setScrollElement(el) | Wire the scroll container DOM node; call from its _onMount. |
| measureElement(el) | Dynamic measurement ref; call from each item's _onMount, and measureElement(null) from _onRemove so disconnected nodes are unobserved. The item element must carry "data-index": item.index — the virtualizer reads the index from that attribute and skips measurement (with a console warning) when it is missing. |
| scrollToIndex(index, opts?) / scrollToOffset(offset, opts?) | Imperative scrolling. |
| scrollBy(delta, opts?) | Relative scroll by a pixel delta. |
| scrollToEnd(opts?) | Scroll to the last item. |
| setOptions(opts) | Update count and other options. Preserves measured sizes; call virtualizer.measure() to force a full remeasure. |
| virtualizer | The underlying Virtualizer — the full virtual-core API. |
| version(l) | Raw reactive change counter. |
| destroy() | Detach observers; call from _onRemove. |
Options are the virtual-core VirtualizerOptions minus getScrollElement (the adapter owns it); observeElementRect, observeElementOffset, scrollToFn, and onChange default to the DOM implementations but can be overridden (e.g. for window virtualization).
Window virtualization
For content that scrolls with the browser window instead of a fixed-height container, use createWindowVirtualizer — same handle contract as createVirtualizer with TScroll fixed to Window and the observeWindowRect / observeWindowOffset / windowScroll defaults filled in (no as any casts):
import { createWindowVirtualizer } from "@domphy/virtual/domphy"
const list = createWindowVirtualizer<HTMLDivElement>({
count: rows.length,
estimateSize: () => 52,
})
const App = {
div: [/* spacer + absolute-positioned items, same as element mode */],
_onMount: () => list.setScrollElement(window),
_onRemove: () => list.destroy(),
}The factory touches no DOM globals at construction, so it is SSR-safe; window is only passed once the tree mounts client-side.
