308

Virtualized

A virtualized 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

Accessibility

Virtualization only affects what's mounted in the DOM, not the semantics of the component being virtualized — a virtualized <Select /> or <Combobox /> keeps the same roles, states and keyboard behavior as its non-virtualized counterpart, since <VirtualizedVirtualizer /> renders its children directly inside the existing listbox/popover markup.

Because off-screen items aren't in the DOM, avoid relying on browser "find in page" or assistive technology virtual cursor navigation to reach items outside the rendered window — users should navigate with the component's own keyboard interactions (arrow keys, typeahead) instead, which keep the active item mounted and in view via keepMounted and scrollToIndex.

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, …).