Skip to content

Virtualizer

A headless windowing primitive that renders only the visible slice of a huge items array. Row heights can be uniform, known-variable, or measured at runtime.

Import

import { Virtualizer } from "@hyzer-labs/ui"

Demo

Tabular data

Windowing a real <table> does not work: a <tr> outside a <table> loses its row semantics. For tabular data, prefer the real Table component up to some thousands of rows. Past that, see the Virtualized table pattern, which builds ARIA table semantics (role="table", "row", "columnheader", "cell") around this component instead.

10,000 rows, but the DOM only ever holds the visible slice plus overscan. Inspect the element and you will find only a handful of .hz-virtualizer-row nodes at any time.

Row 1 (#0)
Row 2 (#1)
Row 3 (#2)
Row 4 (#3)
Row 5 (#4)
Row 6 (#5)
Row 7 (#6)
Row 8 (#7)
Row 9 (#8)
Row 10 (#9)
Row 11 (#10)
Row 12 (#11)
Row 13 (#12)
Row 14 (#13)
const items = Array.from({ length: 10000 }, (_, i) => `Row ${i + 1}`);

<Virtualizer {items} itemHeight={32} height={320}>
	{#snippet row(item, index)}
		<div class="demo-row">{item} <small>(#{index})</small></div>
	{/snippet}
</Virtualizer>

Props

NameTypeDefaultNote
itemsT[]Required.
itemHeightnumber | ((item: T, index: number) => number)Required. A fixed px height (uniform), or a per-item height function (known-variable). The estimate/seed when measure is true.
heightnumberOptional — omit for fluid. Viewport extent in px for fixed, SSR-exact windowing. Omitted, the viewport is fluid: CSS-size it and the component measures its own box at runtime.
measurebooleanfalseRuntime-measures each rendered row via ResizeObserver; itemHeight becomes the seed estimate for unmeasured rows.
overscannumber3Extra rows rendered above/below the visible span.
rowSnippet<[T, number]>Required. Renders one row — see the row snippet signature below.
classstringMerged after the hz-virtualizer class.

itemHeight (union)

NameTypeDefaultNote
numbernumberUniform row height in px — the O(1) fast path (no measure).
(item, index) => number(item: T, index: number) => numberKnown-variable — a per-row height resolved from the item/index.

row snippet — Snippet<[item, index]>

NameTypeDefaultNote
itemTThe array element rendered by this row.
indexnumberThe item's absolute index in items — not the window-local render position — so keys, striping, and aria-posinset stay correct despite windowing.

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-virtualizer

Part classes

HookValuesStyles
.hz-virtualizer-sizerchild elementFull-height spacer that gives the scroller its scroll range.
.hz-virtualizer-windowchild elementThe translated window holding the rendered rows.
.hz-virtualizer-rowchild elementPer-row wrapper. Also load-bearing at runtime — measured mode queries it — so it is stable by contract, not just by convention.

Accessibility

Virtualizer is role-neutral by design. It applies no role, aria-*, or tabindex of its own. It is a rendering optimization rather than a widget, so all semantics come from the row snippet and ...rest on the viewport.

Windowing removes off-screen rows from the DOM, so any count-dependent semantics must be supplied explicitly. Set aria-setsize to the total item count and aria-posinset to the row's absolute index plus one, using the absolute index the snippet receives. That way assistive tech announces 'item N of total' correctly despite the elided DOM. See the List semantics demo below.

A keyboard-scrollable viewport is opt-in: add a tabindex of 0 (plus a role/label) through ...rest. The component adds no key handling of its own.

Virtualization can scroll a focused row out of the DOM, a known windowing hazard. Patterns that need a persistently focused off-screen row (for example, aria-activedescendant listboxes) should keep the active row rendered rather than reaching for the raw Virtualizer.