Headless UI for virtualizing scrollable elements in React
—
React-specific hooks that provide virtualized scrolling for elements and windows with automatic state management and optimal re-rendering performance.
Creates a virtualizer instance for element-based scrolling with automatic React integration and state management.
/**
* React hook for element-based virtualization
* @param options - Virtualization configuration with element-specific defaults
* @returns Virtualizer instance with React integration
*/
export function useVirtualizer<TScrollElement extends Element, TItemElement extends Element>(
options: PartialKeys<
VirtualizerOptions<TScrollElement, TItemElement>,
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
>
): Virtualizer<TScrollElement, TItemElement>;The hook automatically provides default implementations for:
observeElementRect: Uses ResizeObserver for element size changesobserveElementOffset: Monitors scroll position changesscrollToFn: Handles scrolling with element.scrollTo()Usage Examples:
import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
// Basic list virtualization
function BasicList({ items }: { items: string[] }) {
const parentRef = React.useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
}
// Horizontal scrolling
function HorizontalList({ items }: { items: any[] }) {
const parentRef = React.useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 120,
horizontal: true,
});
return (
<div ref={parentRef} style={{ width: '400px', overflowX: 'auto' }}>
<div style={{ width: `${virtualizer.getTotalSize()}px`, height: '100px', position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: `${virtualItem.size}px`,
transform: `translateX(${virtualItem.start}px)`,
}}
>
Item {virtualItem.index}
</div>
))}
</div>
</div>
);
}
// Dynamic sizing with measurement
function DynamicList({ items }: { items: { content: string; height?: number }[] }) {
const parentRef = React.useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => items[index]?.height ?? 50,
overscan: 3,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={(node) => virtualizer.measureElement(node)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div style={{ padding: '8px' }}>
{items[virtualItem.index]?.content}
</div>
</div>
))}
</div>
</div>
);
}Creates a virtualizer instance for window-based scrolling, ideal for full-page virtualization scenarios.
/**
* React hook for window-based virtualization
* @param options - Virtualization configuration with window-specific defaults
* @returns Virtualizer instance configured for window scrolling
*/
export function useWindowVirtualizer<TItemElement extends Element>(
options: PartialKeys<
VirtualizerOptions<Window, TItemElement>,
| 'getScrollElement'
| 'observeElementRect'
| 'observeElementOffset'
| 'scrollToFn'
>
): Virtualizer<Window, TItemElement>;The hook automatically provides default implementations for:
getScrollElement: Returns the window objectobserveElementRect: Uses window resize eventsobserveElementOffset: Monitors window scroll positionscrollToFn: Handles scrolling with window.scrollTo()initialOffset: Uses current window.scrollY positionUsage Examples:
import React from 'react';
import { useWindowVirtualizer } from '@tanstack/react-virtual';
// Full-page virtualization
function WindowVirtualizedList({ items }: { items: string[] }) {
const virtualizer = useWindowVirtualizer({
count: items.length,
estimateSize: () => 100,
overscan: 5,
});
return (
<div style={{ paddingTop: '200px' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div style={{ padding: '16px', border: '1px solid #ccc' }}>
{items[virtualItem.index]}
</div>
</div>
))}
</div>
</div>
);
}
// Infinite scroll with window virtualization
function InfiniteScrollList() {
const [items, setItems] = React.useState<string[]>(
Array.from({ length: 100 }, (_, i) => `Item ${i}`)
);
const [isLoading, setIsLoading] = React.useState(false);
const virtualizer = useWindowVirtualizer({
count: items.length,
estimateSize: () => 80,
overscan: 10,
});
const virtualItems = virtualizer.getVirtualItems();
const lastItem = virtualItems[virtualItems.length - 1];
React.useEffect(() => {
if (!lastItem || isLoading) return;
if (lastItem.index >= items.length - 1) {
setIsLoading(true);
// Simulate loading more items
setTimeout(() => {
setItems(prev => [
...prev,
...Array.from({ length: 50 }, (_, i) => `Item ${prev.length + i}`)
]);
setIsLoading(false);
}, 1000);
}
}, [lastItem?.index, items.length, isLoading]);
return (
<div>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualItems.map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
padding: '8px',
borderBottom: '1px solid #eee',
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
{isLoading && <div style={{ padding: '20px', textAlign: 'center' }}>Loading...</div>}
</div>
);
}Both hooks accept the same base VirtualizerOptions with certain defaults provided:
export interface RequiredVirtualizerOptions {
/** Total number of items to virtualize */
count: number;
/** Function to estimate the size of each item */
estimateSize: (index: number) => number;
}export interface CommonVirtualizerOptions {
/** Enable horizontal scrolling instead of vertical */
horizontal?: boolean;
/** Number of items to render outside the visible area */
overscan?: number;
/** Padding at the start of the scroll area */
paddingStart?: number;
/** Padding at the end of the scroll area */
paddingEnd?: number;
/** Number of lanes for grid layout */
lanes?: number;
/** Gap between items in pixels */
gap?: number;
/** Enable debug logging */
debug?: boolean;
/** Enable right-to-left layout */
isRtl?: boolean;
/** Custom function to extract keys from indices */
getItemKey?: (index: number) => Key;
/** Custom function to extract visible range */
rangeExtractor?: (range: Range) => Array<number>;
/** Custom element measurement function */
measureElement?: (element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer<TScrollElement, TItemElement>) => number;
}Both hooks use React's state management to trigger re-renders only when necessary:
flushSync for smooth scrollinggetItemKey for items with stable identifiersmeasureElement ref callback for accurate sizing of dynamic contentInstall with Tessl CLI
npx tessl i tessl/npm-tanstack--react-virtual