Utils
use-virtual-scroll
Fixed-height list virtualization hook returning container props, the render window and scroll commands
Installation
$npx litefy@latest add use-virtual-scroll
$pnpm dlx litefy@latest add use-virtual-scroll
$yarn dlx litefy@latest add use-virtual-scroll
$bun --bun litefy@latest add use-virtual-scroll
Usage
A headless hook: spread containerProps onto the scroll container, size an inner spacer with totalHeight, and absolutely position the rows listed in visibleItems. Only rows inside the window (plus overscan) are rendered.
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
Row 7
Row 8
Row 9
Row 10
Row 11
Row 12
Row 13
"use client";import { useVirtualScroll } from "@/ui";const ROWS = Array.from({ length: 500 }, (_, i) => `Row ${i + 1}`);export default function VirtualScrollBasicDemo() { const virtual = useVirtualScroll({ itemCount: ROWS.length, itemHeight: 40, visibleCount: 8, }); return ( <div className="flex flex-col items-center gap-4 py-4"> <div className="flex gap-2"> <button type="button" onClick={() => virtual.scrollToIndex(250, "center")} className="px-3 py-1.5 text-sm border rounded hover:bg-primary-accent" > Jump to 250 </button> <button type="button" onClick={virtual.scrollToTop} className="px-3 py-1.5 text-sm border rounded hover:bg-primary-accent" > Top </button> <button type="button" onClick={virtual.scrollToBottom} className="px-3 py-1.5 text-sm border rounded hover:bg-primary-accent" > Bottom </button> </div> <div {...virtual.containerProps} className="w-full max-w-sm overflow-auto rounded-md border" > <div className="relative" style={{ height: virtual.totalHeight }}> {virtual.visibleItems.map(({ index, top }) => ( <div key={index} style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 40, transform: `translateY(${top}px)`, }} className="flex items-center px-4 text-sm border-b" > {ROWS[index]} </div> ))} </div> </div> </div> );}import { useVirtualScroll } from "@/ui";
function List({ rows }: { rows: string[] }) {
const virtual = useVirtualScroll({ itemCount: rows.length, itemHeight: 40, visibleCount: 8 });
return (
<div {...virtual.containerProps} className="overflow-auto">
<div style={{ height: virtual.totalHeight }} className="relative">
{virtual.visibleItems.map(({ index, top }) => (
<div key={index} style={{ position: "absolute", top: 0, width: "100%", height: 40, transform: `translateY(${top}px)` }}>
{rows[index]}
</div>
))}
</div>
</div>
);
}API Reference
useVirtualScroll
function useVirtualScroll(opts: UseVirtualScrollOptions): UseVirtualScrollResultUseVirtualScrollOptions
| Property | Type | Default | Description |
|---|---|---|---|
itemCount | number | - | Total number of items |
itemHeight | number | - | Fixed row height in pixels |
visibleCount | number | 5 | Rows visible at once; the container height is visibleCount * itemHeight |
overscan | number | 5 | Extra rows rendered above and below the window |
onScroll | (scrollTop: number) => void | - | Scroll callback |
UseVirtualScrollResult
| Property | Type | Description |
|---|---|---|
containerProps | { ref, style, onScroll } | Spread onto the scroll container; style sets the fixed height |
totalHeight | number | Spacer height, itemCount * itemHeight |
visibleItems | { index: number; top: number }[] | Rows in the render window with their Y offsets |
scrollToIndex | (index: number, align?: 'start' | 'center' | 'end') => void | Scroll a row into view |
scrollToTop | () => void | Scroll to the first row |
scrollToBottom | () => void | Scroll to the last row |
Notes:
- Fixed row heights only; measured variable heights are not supported.
- The container height is derived from
visibleCount * itemHeight; to size the container yourself, overridecontainerProps.style.heightafter spreading.