Utilities

Virtualizer

Headless virtual collection measurement helpers for rendering only the visible part of large lists.

When to Use

Use Virtualizer when a very long list would create too many DOM elements and slow the page down. Keep a normal semantic list when the collection is small; virtualization adds measurement, positioning, focus, and accessibility work that is unnecessary for ordinary lists.

Features

  • Computes visible virtual items from item count, scroll offset, viewport size, and overscan.
  • Supports measured item sizes with ResizeObserver.
  • Supports stable item keys.
  • Provides scrollToOffset and scrollToIndex helpers.
  • Keeps DOM structure, ARIA roles, keyboard behavior, and styling in the component that uses it.

Import

import {
  useVirtualizer,
  getVirtualItems,
  getVirtualOffsetForIndex,
  getVirtualScrollOffsetForIndex,
  getVirtualTotalSize,
} from "@flowstack-ui/atom";

Anatomy

Virtualizer is a hook and utility set, not a rendered component.

useVirtualizer({ count, estimateSize });
virtualizer.scrollToIndex(index, { align });
getVirtualItems(options);
getVirtualTotalSize(count, getItemSize);
getVirtualOffsetForIndex(count, index, getItemSize);
getVirtualScrollOffsetForIndex(options);

API Reference

useVirtualizer

Measures a scroll container and returns the visible item range.

OptionTypeDefault
countnumberrequired
estimateSize(index: number) => numberrequired
overscannumber1
getItemKey(index: number) => string | numberitem index
Return valueType
scrollRefRefCallback<HTMLElement>
scrollElementHTMLElement | null
itemsVirtualItem[]
totalSizenumber
scrollOffsetnumber
viewportSizenumber
measureElement(index, element) => void
getItemRef(index) => RefCallback<HTMLElement>
getItemSize(index) => number
scrollToOffset(offset) => void
scrollToIndex(index, options?) => void

scrollToIndex

Scrolls the registered element just enough to reveal an item, or aligns it to the requested viewport position.

OptionTypeDefault
align"start" | "center" | "end" | "auto""auto"

getVirtualItems

Pure helper for computing visible virtual items.

OptionTypeDefault
countnumberrequired
scrollOffsetnumberrequired
viewportSizenumberrequired
overscannumber1
getItemSize(index: number) => numberrequired
getItemKey(index: number) => string | numberitem index

getVirtualTotalSize

Pure helper for computing the total scrollable size.

ArgumentType
countnumber
getItemSize(index: number) => number

getVirtualOffsetForIndex

Pure helper for computing the starting offset of an item.

ArgumentType
countnumber
indexnumber
getItemSize(index: number) => number

getVirtualScrollOffsetForIndex

Pure helper for computing the scroll offset needed to reveal an item.

OptionTypeDefault
countnumberrequired
indexnumberrequired
align"start" | "center" | "end" | "auto""auto"
scrollOffsetnumberrequired
viewportSizenumberrequired
getItemSize(index: number) => numberrequired

VirtualItem

Describes one rendered window item and its coordinates in the full collection.

PropertyType
indexnumber
keystring | number
startnumber
endnumber
sizenumber

Examples

Basic List

The inline height, overflow, and positioning values are required geometry for virtualization; they are not Atom component styles.

import { useCallback } from "react";
import { useVirtualizer } from "@flowstack-ui/atom";

const items = Array.from({ length: 1_000 }, (_, index) => `Item ${index + 1}`);

export default function VirtualList() {
  const estimateSize = useCallback(() => 44, []);
  const virtualizer = useVirtualizer({
    count: items.length,
    estimateSize,
    overscan: 4,
  });

  return (
    <>
      <button type="button" onClick={() => virtualizer.scrollToIndex(500)}>
        Jump to item 501
      </button>
      <div
        ref={virtualizer.scrollRef}
        role="list"
        tabIndex={0}
        style={{ height: 320, overflow: "auto" }}
      >
        <div style={{ height: virtualizer.totalSize, position: "relative" }}>
          {virtualizer.items.map((virtualItem) => (
            <div
              key={virtualItem.key}
              ref={virtualizer.getItemRef(virtualItem.index)}
              role="listitem"
              aria-posinset={virtualItem.index + 1}
              aria-setsize={items.length}
              style={{
                position: "absolute",
                top: 0,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              {items[virtualItem.index]}
            </div>
          ))}
        </div>
      </div>
    </>
  );
}

Accessibility

Virtualizer does not create semantics by itself. The scroll container and rendered items must still use the correct semantic component for the UI being built.

  • Add keyboard access to scroll containers when needed, such as tabIndex={0}.
  • Preserve correct aria-setsize, aria-posinset, row indexes, or list semantics when virtualizing semantic collections.
  • Keep focusable active items mounted, or move focus before removing an item from the DOM.
  • Data attributes: Virtualizer renders no wrapper and adds no data attributes, roles, or ARIA attributes.
  • Native scrolling supplies keyboard scrolling when the consumer makes the scroll container focusable.

Changelog

See CHANGELOG.md.