307

Virtualizer

A virtualizer component that allows you to efficiently render large lists and tabular data.

"use client";

import { Card, CardContent } from "@/components/ui/card";

Introduction

Efficiently rendering large lists and tabular data is a common challenge in UI development. By leveraging virtualization, we can significantly enhance performance.

List Virtualization

Rendering only visible rows of content in a dynamic list instead of the entire list using virtualization.

After exploring various libraries, I found virtua to be one of the most intuitive and effective choices. Its straightforward API allows seamless integration with other tools like @dnd-kit and Base UI, making it easy to virtualize different components effectively.

This documentation serves as a guide to help you understand how to use the <Virtualized /> component. Feel free to create your own abstraction of the virtualized components, e.g. <VirtualizedScrollArea />, <VirtualizedSelect />, etc.

Installation

pnpm dlx shadcn@latest add junwen-k/ui-x/virtualized

Examples

Default

"use client";

import { Card, CardContent } from "@/components/ui/card";

Horizontal

"use client";

import { Card, CardContent } from "@/components/ui/card";

Grid

"use client";

import { Card, CardContent } from "@/components/ui/card";

Table

A list of your recent invoices.
InvoiceStatusMethodAmount
Total$494,231.00
"use client";

import { faker } from "@faker-js/faker";

Command

"use client";

import * as React from "react";

Combobox

"use client";

import * as React from "react";

Select

"use client";

import * as React from "react";

Scroll Area

"use client";

import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";

Sortable

"use client";

import { arrayMove } from "@dnd-kit/sortable";

Combobox

To virtualize a <Combobox /> component, you'll need to use <Virtualized /> component.

Setup scollRef

Attach the scrollRef to the <ComboboxContent /> with <Virtualized />, as it serves as the scrollable container:

export function VirtualizedComboboxDemo() {
  return (
    <Combobox>
      {/* ... */}
      <Virtualized render={<ComboboxContent />}>
        {/* ... */}
        <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
        {/* ... */}
      </Virtualized>
    </Combobox>
  );
}

Handle filtering

Control the inputValue and pass shouldFilter={false} to the <Combobox />, as the default filtering does not support virtualization. You will also need to filter items manually:

export function VirtualizedComboboxDemo() {
  // ...
  const [inputValue, setInputValue] = React.useState("");
 
  const filtered = React.useMemo(() => {
    if (!inputValue) {
      return items;
    }
 
    return items.filter((item) =>
      item.label.toLowerCase().includes(inputValue.toLowerCase()),
    );
  }, [inputValue]);
 
  return (
    <Combobox
      // ...
      shouldFilter={false}
      inputValue={inputValue}
      onInputValueChange={setInputValue}
      // ...
    >
      {/* ... */}
    </Combobox>
  );
}
"use client";

import * as React from "react";

Scroll Area

To virtualize a <ScrollArea /> component, you'll need to use the primitive components directly. shadcn/ui's version includes its own abstraction, which does not support virtualization out of the box.

Setup scrollRef

Attach the scrollRef to the <ScrollAreaPrimitive.Viewport /> by rendering it as <Virtualized />, as it serves as the scrollable container:

export function VirtualizedScrollAreaDemo() {
  return (
    <ScrollAreaPrimitive.Root className="relative h-72 w-48 overflow-hidden rounded-md border">
      <ScrollAreaPrimitive.Viewport
        render={<Virtualized />}
        className="size-full rounded-[inherit]"
      >
        {/* ... */}
        <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
        {/* ... */}
      </ScrollAreaPrimitive.Viewport>
      <ScrollBar />
      <ScrollAreaPrimitive.Corner />
    </ScrollAreaPrimitive.Root>
  );
}
"use client";

import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";

Select

To virtualize a <Select /> component, render its <SelectContent /> as a <Virtualized /> viewport. Since the virtualized viewport manages its own scrolling, set alignItemWithTrigger={false} so the content is positioned like a popover:

Setup scrollRef

Attach the scrollRef to the <SelectContent /> by rendering it as <Virtualized />, as it serves as the scrollable container:

export function VirtualizedSelectDemo() {
  return (
    <Select items={items}>
      {/* ... */}
      <SelectContent
        alignItemWithTrigger={false}
        className="max-h-96"
        render={<Virtualized />}
      >
        <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
      </SelectContent>
    </Select>
  );
}

Handle scroll position and focus

To replicate the scroll position and focus behavior of <Select />, manage the open and value states, and access the virtualizer instance using imperative methods with the ref:

export function VirtualizedSelectDemo() {
  const [value, setValue] = React.useState<string | null>(null);
  const [open, setOpen] = React.useState(false);
 
  const virtualizerRef = React.useRef<VirtualizerHandle>(null);
  const viewportRef = React.useRef<HTMLDivElement>(null);
 
  const activeIndex = React.useMemo(
    () => items.findIndex((item) => item.value === value),
    [value],
  );
 
  React.useLayoutEffect(() => {
    if (!open || !value || activeIndex === -1) return;
 
    setTimeout(() => {
      // Recover scroll position.
      virtualizerRef.current?.scrollToIndex(activeIndex, { align: "end" });
 
      const selectedElement = viewportRef.current?.querySelector(
        "[data-selected]",
      ) as HTMLElement;
 
      // Recover focus.
      selectedElement?.focus({ preventScroll: true });
    });
  }, [open, value, activeIndex]);
 
  return (
    <Select
      open={open}
      onOpenChange={setOpen}
      value={value}
      onValueChange={setValue}
    >
      {/* ... */}
      <SelectContent render={<Virtualized ref={viewportRef} />}>
        <VirtualizedVirtualizer ref={virtualizerRef}>
          {/* ... */}
        </VirtualizedVirtualizer>
      </SelectContent>
      {/* ... */}
    </Select>
  );
}

Ensure active item is always mounted

To ensure the selected item is rendered within the <SelectTrigger />, use the keepMounted prop:

export function VirtualizedSelectDemo() {
  // ...
 
  return (
    <Select
    // ...
    >
      {/* ... */}
      <VirtualizedVirtualizer
        // ...
        keepMounted={activeIndex !== -1 ? [activeIndex] : undefined}
      >
        {/* ... */}
      </VirtualizedVirtualizer>
      {/* ... */}
    </Select>
  );
}
"use client";

import * as React from "react";

Reusable Components

Virtualized Select

API Reference

Thin wrappers around virtua — see the virtua API reference for the full list of underlying props and imperative handle methods.

Virtualized

The scrollable container. Renders a <div> and provides its scroll ref to a <VirtualizedVirtualizer /> inside. Use the render prop to attach the scroll container to an existing component, e.g. render={<ComboboxContent />}.

PropTypeDefaultDescription
renderReactElement | function-Render as a different element.
...propsReact.ComponentProps<"div">-Props spread to the container element.

VirtualizedList

A standalone virtualized list that scrolls itself. Wraps virtua's VList and accepts all of its props (keepMounted, shift, imperative ref, …). Must not be used within a <Virtualized /> — use <VirtualizedVirtualizer /> there instead.

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"The scroll direction of the list.
...propsReact.ComponentProps<typeof VList>-Props spread to virtua's VList.

VirtualizedGrid

A virtualized grid that virtualizes both rows and columns. Wraps virtua's experimental VGrid and accepts all of its props (row, col, cellHeight, cellWidth, …).

VirtualizedVirtualizer

The virtualized viewport for a <Virtualized /> container. Wraps virtua's Virtualizer with scrollRef already wired to the surrounding <Virtualized />, and accepts all of its remaining props (startMargin, keepMounted, imperative ref, …).