Docs
Virtualizer

Virtualizer

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

0
1
2
3
4

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 Radix 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 cross-env REGISTRY_URL=https://ui-x.junwen-k.dev/r pnpm dlx shadcn@latest add virtualized

Examples

Default

0
1
2
3
4

Horizontal

0
1
2
3
4

Grid

0/0
0/1
0/2
1/0
1/1
1/2
2/0
2/1
2/2

Table

A list of your recent invoices.
InvoiceStatusMethodAmount
Total$504,162.00

Combobox

Select

Scroll Area

Tags

v1.2.0-beta.10000
v1.2.0-beta.9999
v1.2.0-beta.9998
v1.2.0-beta.9997
v1.2.0-beta.9996

Sortable

    Item 1
    Description 0
    Item 2
    Description 1
    Item 3
    Description 2
    Item 4
    Description 3
    Item 5
    Description 4

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 asChild>
        <ComboboxContent>
          {/* ... */}
          <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
          {/* ... */}
        </ComboboxContent>
      </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>
  )
}

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.

Create reusable styles (Optional)

You can create reusable styles for the <ScrollArea /> component by exporting variants:

scroll-area.tsx
// ...
 
const scrollAreaVariants = cva("relative overflow-hidden")
 
const scrollAreaViewportVariants = cva("size-full rounded-[inherit]")
 
const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn(scrollAreaVariants(), className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className={scrollAreaViewportVariants()}>
      {children}
    </ScrollAreaPrimitive.Viewport>
    <ScrollBar />
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
 
// ...
 
export {
  // ...
  scrollAreaVariants,
  scrollAreaViewportVariants,
}

Setup scollRef

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

export function VirtualizedScrollAreaDemo() {
  return (
    <ScrollArea
      className={cn(scrollAreaVariants(), "h-72 w-48 rounded-md border")}
    >
      {/* ... */}
      <Virtualized asChild>
        <ScrollAreaViewport className={scrollAreaViewportVariants()}>
          {/* ... */}
          <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
          {/* ... */}
        </ScrollAreaViewport>
      </Virtualized>
      {/* ... */}
    </ScrollArea>
  )
}

Tags

v1.2.0-beta.10000
v1.2.0-beta.9999
v1.2.0-beta.9998
v1.2.0-beta.9997
v1.2.0-beta.9996

Select

To virtualize a <Select /> 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.

Create reusable styles (Optional)

You can create reusable styles for the <Select /> component by exporting variants:

select.tsx
// ...
 
const selectContentVariants = cva(
  "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
  {
    variants: {
      position: {
        popper:
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        "item-aligned": "",
      },
    },
    defaultVariants: {
      position: "item-aligned",
    },
  }
)
 
const selectViewportVariants = cva("p-1", {
  variants: {
    position: {
      popper:
        "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
      "item-aligned": "",
    },
  },
  defaultVariants: {
    position: "item-aligned",
  },
})
 
const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(selectContentVariants({ position }), className)}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(selectViewportVariants({ position }))}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
 
// ...
 
export {
  // ...
  selectContentVariants,
  selectViewportVariants,
}

Setup scollRef

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

export function VirtualizedSelectDemo() {
  return (
    <Select>
      {/* ... */}
      <SelectPortal>
        <SelectContent
          className={selectContentVariants({ position: "popper" })}
          position="popper"
        >
          <SelectViewport
            className={selectViewportVariants({ position: "popper" })}
          >
            <VirtualizedVirtualizer>{/* ... */}</VirtualizedVirtualizer>
          </SelectViewport>
        </SelectContent>
      </SelectPortal>
    </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("")
  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 checkedElement = viewportRef.current?.querySelector(
        "[data-state=checked]"
      ) as HTMLElement
 
      // Recover focus.
      checkedElement?.focus({ preventScroll: true })
    })
  }, [open, value, activeIndex])
 
  return (
    <Select
      open={open}
      onOpenChange={setOpen}
      value={value}
      onValueChange={setValue}
    >
      {/* ... */}
      <SelectViewport
        ref={viewportRef}
        // ...
      >
        <VirtualizedVirtualizer ref={virtualizerRef}>
          {/* ... */}
        </VirtualizedVirtualizer>
      </SelectViewport>
      {/* ... */}
    </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>
  )
}

Reusable Components

Virtualized Select