Virtual List

DnD & Interaction

Virtualized list for rendering large datasets.

Preview

Row 1 — Virtualised for performance
Row 2 — Virtualised for performance
Row 3 — Virtualised for performance
Row 4 — Virtualised for performance
Row 5 — Virtualised for performance
Row 6 — Virtualised for performance
Row 7 — Virtualised for performance
Row 8 — Virtualised for performance
Row 9 — Virtualised for performance
Row 10 — Virtualised for performance
Row 11 — Virtualised for performance
Row 12 — Virtualised for performance
Row 13 — Virtualised for performance
Row 14 — Virtualised for performance
Row 15 — Virtualised for performance
Row 16 — Virtualised for performance

Usage

example.jsx
import { VirtualList } from "@/components/ui/virtual-list";

export default function Example() {
  return <VirtualList />;
}

Source Code

Copy this file into components/ui/virtual-list.jsx in your project.

virtual-list.jsx
"use client";

import { forwardRef, useState, useRef, useCallback, useEffect } from "react";
import { cn } from "@/lib/utils";

const VirtualList = forwardRef(({ className, items = [], itemHeight = 40, overscan = 5, renderItem, height = 400, ...props }, ref) => {
  const containerRef = useRef(null);
  const [scrollTop, setScrollTop] = useState(0);

  const totalHeight = items.length * itemHeight;
  const visibleCount = Math.ceil(height / itemHeight);
  const startIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
  const endIdx = Math.min(items.length, startIdx + visibleCount + overscan * 2);
  const visibleItems = items.slice(startIdx, endIdx);

  const onScroll = useCallback((e) => setScrollTop(e.currentTarget.scrollTop), []);

  return (
    <div ref={(el) => { containerRef.current = el; if (typeof ref === "function") ref(el); else if (ref) ref.current = el; }}
      className={cn("overflow-auto rounded-md border", className)}
      style={{ height }}
      onScroll={onScroll}
      {...props}
    >
      <div style={{ height: totalHeight, position: "relative" }}>
        {visibleItems.map((item, i) => (
          <div key={startIdx + i}
            style={{ position: "absolute", top: (startIdx + i) * itemHeight, height: itemHeight, left: 0, right: 0 }}
          >
            {renderItem ? renderItem(item, startIdx + i) : <div className="flex items-center px-3 text-sm">{String(item)}</div>}
          </div>
        ))}
      </div>
    </div>
  );
});
VirtualList.displayName = "VirtualList";

export { VirtualList };

Quick Install

Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.

npm install clsx tailwind-merge