Domphy

Window Virtualizer

By default, createVirtualizer virtualizes items inside a fixed-height scrollable div. When your content should scroll with the browser window (the whole page scrolls), use the window-specific helpers instead.

Setup

Import the window observers and scroll function from @domphy/virtual:

import {
  observeWindowRect,
  observeWindowOffset,
  windowScroll,
} from "@domphy/virtual"
import { createVirtualizer } from "@domphy/virtual/domphy"

Pass them as options, then wire window as the scroll element from _onMount:

const list = createVirtualizer({
  count: rows.length,
  estimateSize: () => 50,
  observeElementRect: observeWindowRect as any,
  observeElementOffset: observeWindowOffset as any,
  scrollToFn: windowScroll as any,
  overscan: 5,
})

// In the container element:
_onMount: () => list.setScrollElement(window as any)

The as any casts are needed because the createVirtualizer generic constrains TScroll extends Element, while window is a Window. The runtime behaviour is correct — the Window overloads in the virtualizer core handle all window-specific reads.

Full example

import { observeWindowRect, observeWindowOffset, windowScroll } from "@domphy/virtual"
import { createVirtualizer } from "@domphy/virtual/domphy"
import { themeColor, themeSpacing } from "@domphy/theme"

const rows = Array.from({ length: 10_000 }, (_, i) => `Row ${i + 1}`)

const list = createVirtualizer({
  count: rows.length,
  estimateSize: () => 52,
  observeElementRect: observeWindowRect as any,
  observeElementOffset: observeWindowOffset as any,
  scrollToFn: windowScroll as any,
  overscan: 5,
})

const App = {
  // No fixed height — the document height equals the spacer height
  div: [
    {
      div: (l) =>
        list.getVirtualItems(l).map((item) => ({
          _key: item.key,
          div: rows[item.index],
          style: {
            position: "absolute",
            top: 0,
            left: 0,
            width: "100%",
            height: `${item.size}px`,
            transform: `translateY(${item.start}px)`,
            display: "flex",
            alignItems: "center",
            paddingInline: themeSpacing(3),
            boxSizing: "border-box",
            borderBottom: (cl) => `1px solid ${themeColor(cl, "shift-3")}`,
          },
        })),
      style: {
        position: "relative",
        height: (l) => `${list.getTotalSize(l)}px`,
      },
    },
  ],
  _onMount: () => list.setScrollElement(window as any),
  _onRemove: () => list.destroy(),
}

Key differences from element-based virtualization:

AspectElement containerWindow
Container heightFixed (e.g. 600px)Not set — page scrolls
Scroll elementnode.domElementwindow
Rect observerobserveElementRectobserveWindowRect
Offset observerobserveElementOffsetobserveWindowOffset
Scroll functionelementScrollwindowScroll

scrollMargin

When the virtual list starts below the top of the page (e.g. after a fixed header), set scrollMargin to the header height. This shifts all item.start values so they are relative to the document top, not the list container top:

const HEADER_HEIGHT = 64

const list = createVirtualizer({
  count: rows.length,
  estimateSize: () => 52,
  scrollMargin: HEADER_HEIGHT,  // items begin 64px from the document top
  observeElementRect: observeWindowRect as any,
  observeElementOffset: observeWindowOffset as any,
  scrollToFn: windowScroll as any,
})

const App = {
  div: [
    // Fixed header — not part of the virtual list
    {
      header: "My App",
      style: {
        position: "sticky",
        top: 0,
        height: `${HEADER_HEIGHT}px`,
        background: (cl) => themeColor(cl, "surface"),
        zIndex: 10,
        borderBottom: (cl) => `1px solid ${themeColor(cl, "shift-3")}`,
        display: "flex",
        alignItems: "center",
        paddingInline: themeSpacing(4),
      },
    },
    // Virtual list body
    {
      div: [
        {
          div: (l) =>
            list.getVirtualItems(l).map((item) => ({
              _key: item.key,
              div: rows[item.index],
              style: {
                position: "absolute",
                top: 0,
                height: `${item.size}px`,
                transform: `translateY(${item.start}px)`,
                width: "100%",
                paddingInline: themeSpacing(3),
                display: "flex",
                alignItems: "center",
                boxSizing: "border-box",
              },
            })),
          style: {
            position: "relative",
            height: (l) => `${list.getTotalSize(l)}px`,
          },
        },
      ],
      _onMount: () => list.setScrollElement(window as any),
      _onRemove: () => list.destroy(),
    },
  ],
}

Variable heights with window scroll

measureElement works identically with window scroll:

const list = createVirtualizer({
  count: posts.length,
  estimateSize: () => 80,
  observeElementRect: observeWindowRect as any,
  observeElementOffset: observeWindowOffset as any,
  scrollToFn: windowScroll as any,
})

// Attach measureElement to each item:
const PostItem = (item: VirtualItem) => ({
  _key: item.key,
  article: PostContent(posts[item.index]),
  _onMount: (node) =>
    list.measureElement(node.domElement as HTMLElement),
  style: {
    position: "absolute",
    top: 0,
    width: "100%",
    transform: `translateY(${item.start}px)`,
  },
})

scrollToIndex and scrollToOffset

Both methods work the same. The underlying windowScroll function calls window.scrollTo:

// Scroll to item 1000, smooth
list.scrollToIndex(1000, { align: "start", behavior: "smooth" })

// Jump to top of page
list.scrollToOffset(0)

iOS WebKit

On iOS, writing scrollTop during a momentum scroll cancels the in-flight inertia. @domphy/virtual detects iOS WebKit automatically via navigator.userAgent and defers scroll-position corrections (triggered by dynamic item size changes) until the scroll settles completely — no configuration required. This applies to both element and window scroll modes.