Headless UI for virtualizing scrollable elements in React
—
The core virtualization engine class that manages all aspects of virtual scrolling including item measurement, range calculation, scroll positioning, and DOM element lifecycle management.
The main virtualization engine that handles all scrolling, measurement, and rendering calculations.
/**
* Core virtualization engine for managing virtual scrolling
* @template TScrollElement - Type of the scroll container (Element or Window)
* @template TItemElement - Type of the virtualized item elements
*/
export class Virtualizer<TScrollElement extends Element | Window, TItemElement extends Element> {
constructor(opts: VirtualizerOptions<TScrollElement, TItemElement>);
// Configuration
setOptions(opts: VirtualizerOptions<TScrollElement, TItemElement>): void;
// Virtual item management
getVirtualItems(): Array<VirtualItem>;
getVirtualIndexes(): Array<number>;
calculateRange(): { startIndex: number; endIndex: number } | null;
// Measurement and sizing
measureElement(node: TItemElement | null | undefined): void;
resizeItem(index: number, size: number): void;
getTotalSize(): number;
measure(): void;
// Scroll control
scrollToIndex(index: number, options?: ScrollToIndexOptions): void;
scrollToOffset(toOffset: number, options?: ScrollToOffsetOptions): void;
scrollBy(delta: number, options?: ScrollToOffsetOptions): void;
// Position calculations
getOffsetForIndex(index: number, align?: ScrollAlignment): [number, ScrollAlignment] | undefined;
getOffsetForAlignment(toOffset: number, align: ScrollAlignment, itemSize?: number): number;
getVirtualItemForOffset(offset: number): VirtualItem | undefined;
// Element utilities
indexFromElement(node: TItemElement): number;
}The Virtualizer instance exposes several readonly properties for accessing current state:
interface VirtualizerState {
/** The current scroll element being observed */
scrollElement: TScrollElement | null;
/** Reference to the target window object */
targetWindow: (Window & typeof globalThis) | null;
/** Whether the virtualizer is currently scrolling */
isScrolling: boolean;
/** Cache of measured virtual items */
measurementsCache: Array<VirtualItem>;
/** Current dimensions of the scroll container */
scrollRect: Rect | null;
/** Current scroll offset position */
scrollOffset: number | null;
/** Current scroll direction when scrolling */
scrollDirection: ScrollDirection | null;
/** Cache of DOM element references */
elementsCache: Map<Key, TItemElement>;
/** Current visible range of items */
range: { startIndex: number; endIndex: number } | null;
}Methods for managing and accessing virtualized items:
/**
* Get the current list of virtual items that should be rendered
* @returns Array of VirtualItem objects representing visible items
*/
getVirtualItems(): Array<VirtualItem>;
/**
* Get the indexes of items that need to be rendered (including overscan)
* @returns Array of item indexes to render
*/
getVirtualIndexes(): Array<number>;
/**
* Calculate the currently visible range of items
* @returns Object with startIndex and endIndex, or null if no items visible
*/
calculateRange(): { startIndex: number; endIndex: number } | null;Usage Examples:
import { Virtualizer } from '@tanstack/react-virtual';
// Create virtualizer instance
const virtualizer = new Virtualizer({
count: 1000,
getScrollElement: () => document.getElementById('scroll-container'),
estimateSize: () => 50,
scrollToFn: (offset, { behavior }, instance) => {
instance.scrollElement?.scrollTo({ top: offset, behavior });
},
observeElementRect: (instance, cb) => {
// ResizeObserver implementation
const observer = new ResizeObserver(entries => {
const entry = entries[0];
if (entry) {
cb({ width: entry.contentRect.width, height: entry.contentRect.height });
}
});
if (instance.scrollElement) observer.observe(instance.scrollElement as Element);
return () => observer.disconnect();
},
observeElementOffset: (instance, cb) => {
// Scroll event implementation
const handler = () => cb((instance.scrollElement as Element).scrollTop, true);
instance.scrollElement?.addEventListener('scroll', handler);
return () => instance.scrollElement?.removeEventListener('scroll', handler);
}
});
// Get items to render
const virtualItems = virtualizer.getVirtualItems();
virtualItems.forEach(item => {
console.log(`Render item ${item.index} at position ${item.start}`);
});Methods for handling dynamic item sizing and measurement:
/**
* Measure a DOM element and update the virtualizer's size cache
* @param node - The DOM element to measure, or null to clean up disconnected elements
*/
measureElement(node: TItemElement | null | undefined): void;
/**
* Manually resize a specific item by index
* @param index - Index of the item to resize
* @param size - New size of the item in pixels
*/
resizeItem(index: number, size: number): void;
/**
* Get the total size of all virtualized content
* @returns Total height (vertical) or width (horizontal) in pixels
*/
getTotalSize(): number;
/**
* Force a complete remeasurement of all items
* Clears the size cache and triggers recalculation
*/
measure(): void;Usage Examples:
// Measure element during render
function VirtualItem({ virtualizer, item, data }) {
return (
<div
data-index={item.index}
ref={(node) => virtualizer.measureElement(node)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${item.start}px)`,
}}
>
{data[item.index]}
</div>
);
}
// Manual resizing after content change
function updateItemContent(virtualizer: Virtualizer, index: number, newContent: string) {
// Update content...
// If we know the new size, update it directly
const newSize = estimateContentHeight(newContent);
virtualizer.resizeItem(index, newSize);
// Or trigger complete remeasurement
virtualizer.measure();
}
// Get container height for styling
function VirtualContainer({ virtualizer }) {
const totalSize = virtualizer.getTotalSize();
return (
<div style={{
height: `${totalSize}px`,
position: 'relative',
}}>
{/* Virtual items */}
</div>
);
}Methods for programmatically controlling scroll position:
/**
* Scroll to a specific item index
* @param index - Index of the item to scroll to
* @param options - Scroll behavior options
*/
scrollToIndex(index: number, options?: ScrollToIndexOptions): void;
/**
* Scroll to a specific offset position
* @param toOffset - Target scroll position in pixels
* @param options - Scroll behavior options
*/
scrollToOffset(toOffset: number, options?: ScrollToOffsetOptions): void;
/**
* Scroll by a relative amount
* @param delta - Amount to scroll in pixels (positive = down/right, negative = up/left)
* @param options - Scroll behavior options
*/
scrollBy(delta: number, options?: ScrollToOffsetOptions): void;
interface ScrollToIndexOptions {
/** How to align the item within the viewport */
align?: ScrollAlignment;
/** Scroll behavior (smooth or instant) */
behavior?: ScrollBehavior;
}
interface ScrollToOffsetOptions {
/** How to align the offset within the viewport */
align?: ScrollAlignment;
/** Scroll behavior (smooth or instant) */
behavior?: ScrollBehavior;
}
type ScrollAlignment = 'start' | 'center' | 'end' | 'auto';
type ScrollBehavior = 'auto' | 'smooth';Usage Examples:
// Scroll to specific items
function scrollToTop(virtualizer: Virtualizer) {
virtualizer.scrollToIndex(0, { align: 'start' });
}
function scrollToBottom(virtualizer: Virtualizer) {
virtualizer.scrollToIndex(virtualizer.options.count - 1, { align: 'end' });
}
function scrollToMiddle(virtualizer: Virtualizer) {
const middleIndex = Math.floor(virtualizer.options.count / 2);
virtualizer.scrollToIndex(middleIndex, { align: 'center', behavior: 'smooth' });
}
// Scroll by relative amounts
function scrollPage(virtualizer: Virtualizer, direction: 'up' | 'down') {
const pageSize = virtualizer.scrollRect?.height ?? 400;
const delta = direction === 'down' ? pageSize : -pageSize;
virtualizer.scrollBy(delta, { behavior: 'smooth' });
}
// Scroll to specific positions
function scrollToPosition(virtualizer: Virtualizer, position: number) {
virtualizer.scrollToOffset(position, { align: 'start' });
}Methods for calculating positions and offsets:
/**
* Get the scroll offset needed to show a specific item
* @param index - Index of the item
* @param align - How to align the item (default: 'auto')
* @returns Tuple of [offset, actualAlignment] or undefined if item not found
*/
getOffsetForIndex(index: number, align?: ScrollAlignment): [number, ScrollAlignment] | undefined;
/**
* Calculate the aligned offset for a given position
* @param toOffset - Target offset position
* @param align - Alignment mode
* @param itemSize - Size of the item being aligned (optional)
* @returns Calculated offset position
*/
getOffsetForAlignment(toOffset: number, align: ScrollAlignment, itemSize?: number): number;
/**
* Find the virtual item at a specific scroll offset
* @param offset - Scroll offset position
* @returns VirtualItem at that position, or undefined if none found
*/
getVirtualItemForOffset(offset: number): VirtualItem | undefined;Utility methods for working with DOM elements:
/**
* Extract the item index from a DOM element's data-index attribute
* @param node - The DOM element to inspect
* @returns The item index, or -1 if not found or invalid
*/
indexFromElement(node: TItemElement): number;Usage Examples:
// Handle click events on virtual items
function handleItemClick(event: MouseEvent, virtualizer: Virtualizer) {
const element = event.target as HTMLElement;
const index = virtualizer.indexFromElement(element);
if (index >= 0) {
console.log(`Clicked item at index ${index}`);
}
}
// Find item at current scroll position
function getCurrentItem(virtualizer: Virtualizer) {
const currentOffset = virtualizer.scrollOffset ?? 0;
const item = virtualizer.getVirtualItemForOffset(currentOffset);
return item;
}
// Calculate positions for custom scroll animations
function animateToItem(virtualizer: Virtualizer, targetIndex: number) {
const offsetInfo = virtualizer.getOffsetForIndex(targetIndex, 'center');
if (offsetInfo) {
const [targetOffset] = offsetInfo;
// Implement custom animation to targetOffset
animateScrollTo(targetOffset);
}
}The Virtualizer requires comprehensive configuration through VirtualizerOptions:
export interface VirtualizerOptions<TScrollElement extends Element | Window, TItemElement extends Element> {
// Required configuration
count: number;
getScrollElement: () => TScrollElement | null;
estimateSize: (index: number) => number;
scrollToFn: (offset: number, options: ScrollOptions, instance: Virtualizer<TScrollElement, TItemElement>) => void;
observeElementRect: (instance: Virtualizer<TScrollElement, TItemElement>, cb: (rect: Rect) => void) => void | (() => void);
observeElementOffset: (instance: Virtualizer<TScrollElement, TItemElement>, cb: (offset: number, isScrolling: boolean) => void) => void | (() => void);
// Optional configuration with defaults
debug?: boolean; // false
initialRect?: Rect; // { width: 0, height: 0 }
onChange?: (instance: Virtualizer<TScrollElement, TItemElement>, sync: boolean) => void;
measureElement?: (element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer<TScrollElement, TItemElement>) => number;
overscan?: number; // 1
horizontal?: boolean; // false
paddingStart?: number; // 0
paddingEnd?: number; // 0
scrollPaddingStart?: number; // 0
scrollPaddingEnd?: number; // 0
initialOffset?: number | (() => number); // 0
getItemKey?: (index: number) => Key; // (index) => index
rangeExtractor?: (range: Range) => Array<number>; // defaultRangeExtractor
scrollMargin?: number; // 0
gap?: number; // 0
indexAttribute?: string; // 'data-index'
initialMeasurementsCache?: Array<VirtualItem>; // []
lanes?: number; // 1
isScrollingResetDelay?: number; // 150
useScrollendEvent?: boolean; // false
enabled?: boolean; // true
isRtl?: boolean; // false
useAnimationFrameWithResizeObserver?: boolean; // false
}
export interface ScrollOptions {
adjustments?: number;
behavior?: ScrollBehavior;
}The Virtualizer uses binary search algorithms to efficiently calculate visible ranges even with thousands of items.
scrollend events where availableInstall with Tessl CLI
npx tessl i tessl/npm-tanstack--react-virtual