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 { Button, 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 variant="outline" onClick={() => virtual.scrollToIndex(250, "center")}>
Jump to 250
</Button>
<Button variant="outline" onClick={virtual.scrollToTop}>
Top
</Button>
<Button variant="outline" onClick={virtual.scrollToBottom}>
Bottom
</Button>
</div>
<div
{...virtual.containerProps}
className="w-full max-w-sm overflow-auto rounded-md border"
>
<ul className="relative" style={{ height: virtual.totalHeight }}>
{virtual.visibleItems.map(({ index, top }) => (
<li
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]}
</li>
))}
</ul>
</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.