Skip to content

Virtualization Table

Handle large datasets efficiently with virtual scrolling.

Open in
Preview with Controlled State
Open in

The Virtualization Table uses virtual scrolling to efficiently render large datasets (1000+ rows) by only rendering visible rows. This provides smooth performance even with 10,000+ rows.

  1. Add the required components:
npx shadcn@latest add table input button dropdown-menu scroll-area
  1. Add dependencies:
npm install @tanstack/react-table @tanstack/react-virtual
  1. Copy the DataTable components into your project. See the Installation Guide for detailed instructions.

We are going to build a table to show products with virtualization. Here’s what our data looks like:

type Product = {
id: string
name: string
category: string
price: number
stock: number
status: "in-stock" | "low-stock" | "out-of-stock"
}
// Generate large dataset
const largeData: Product[] = Array.from({ length: 10000 }, (_, i) => ({
id: `product-${i + 1}`,
name: `Product ${i + 1}`,
category: "Electronics",
price: Math.floor(Math.random() * 500) + 10,
stock: Math.floor(Math.random() * 150),
status: "in-stock",
}))

Let’s start by building a virtualized table.

First, we’ll define our columns.

columns.tsx
"use client"
import {
DataTableColumnHeader,
DataTableColumnTitle,
DataTableColumnSortMenu,
} from "@/components/niko-table/components"
import type { DataTableColumnDef } from "@/components/niko-table/types"
export type Product = {
id: string
name: string
category: string
price: number
stock: number
status: "in-stock" | "low-stock" | "out-of-stock"
}
export const columns: DataTableColumnDef<Product>[] = [
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Name" },
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
},
{
accessorKey: "category",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Category" },
},
{
accessorKey: "price",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Price" },
cell: ({ row }) => {
const price = row.getValue("price") as number
return <div className="font-mono">${price.toFixed(2)}</div>
},
},
{
accessorKey: "stock",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Stock" },
},
{
accessorKey: "status",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Status" },
},
]

Next, we’ll create a virtualized table.

virtualization-table.tsx
"use client"
import {
DataTableRoot,
DataTable,
DataTableVirtualizedHeader,
DataTableVirtualizedBody,
DataTableVirtualizedEmptyBody,
DataTableVirtualizedSkeleton,
} from "@/components/niko-table/core"
import {
DataTableToolbarSection,
DataTablePagination,
DataTableSearchFilter,
DataTableViewMenu,
DataTableColumnHeader,
DataTableColumnTitle,
DataTableColumnSortMenu,
} from "@/components/niko-table/components"
import type { DataTableColumnDef } from "@/components/niko-table/types"
type Product = {
id: string
name: string
category: string
price: number
stock: number
status: "in-stock" | "low-stock" | "out-of-stock"
}
const columns: DataTableColumnDef<Product>[] = [
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Name" },
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
},
{
accessorKey: "category",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Category" },
},
{
accessorKey: "price",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Price" },
cell: ({ row }) => {
const price = row.getValue("price") as number
return <div className="font-mono">${price.toFixed(2)}</div>
},
},
{
accessorKey: "stock",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Stock" },
},
{
accessorKey: "status",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Status" },
},
]
export function VirtualizationTable({ data }: { data: Product[] }) {
return (
<DataTableRoot
data={data}
columns={columns}
config={{
enablePagination: true,
enableSorting: true,
enableFilters: true,
}}
>
<DataTableToolbarSection>
<DataTableSearchFilter placeholder="Search products..." />
<DataTableViewMenu />
</DataTableToolbarSection>
<DataTable height={600} className="rounded-lg border">
<DataTableVirtualizedHeader />
<DataTableVirtualizedBody>
<DataTableVirtualizedSkeleton rows={5} />
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>
</DataTable>
<DataTablePagination pageSizeOptions={[50, 100, 200, 500]} />
</DataTableRoot>
)
}

Virtualized version of the table header.

<DataTableVirtualizedHeader />

Virtualized version of the table body. Only renders visible rows.

<DataTableVirtualizedBody>
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>

Empty state component for virtualized tables.

<DataTableVirtualizedEmptyBody />

Skeleton loading state for virtualized tables. Shows skeleton rows during data fetching.

<DataTableVirtualizedBody>
<DataTableVirtualizedSkeleton rows={5} />
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>

Simple loading spinner for virtualized tables. Shows a centered loading indicator.

<DataTableVirtualizedBody>
<DataTableVirtualizedLoading />
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>

Note: Use DataTableVirtualizedSkeleton for a more detailed loading state that mimics the table structure, or DataTableVirtualizedLoading for a simple spinner.

Virtualization requires a fixed height on the DataTable component:

<DataTable height={600} className="rounded-lg border">
<DataTableVirtualizedHeader />
<DataTableVirtualizedBody>
<DataTableVirtualizedSkeleton rows={5} />
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>
</DataTable>

Manage table state externally for full control:

import { useState } from "react"
import type {
SortingState,
ColumnFiltersState,
PaginationState,
} from "@tanstack/react-table"
export function ControlledVirtualizationTable({ data }: { data: Product[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 50,
})
const [globalFilter, setGlobalFilter] = useState("")
return (
<DataTableRoot
data={data}
columns={columns}
config={{
enablePagination: true,
enableSorting: true,
enableFilters: true,
}}
state={{
sorting,
columnFilters,
pagination,
globalFilter,
}}
onSortingChange={setSorting}
onColumnFiltersChange={setColumnFilters}
onPaginationChange={setPagination}
onGlobalFilterChange={setGlobalFilter}
>
<DataTableToolbarSection>
<DataTableSearchFilter placeholder="Search products..." />
<DataTableViewMenu />
</DataTableToolbarSection>
<DataTable height={600} className="rounded-lg border">
<DataTableVirtualizedHeader />
<DataTableVirtualizedBody>
<DataTableVirtualizedSkeleton rows={5} />
<DataTableVirtualizedEmptyBody />
</DataTableVirtualizedBody>
</DataTable>
<DataTablePagination pageSizeOptions={[50, 100, 200, 500]} />
</DataTableRoot>
)
}

✅ Use Virtualization Table when:

  • You have 1000+ rows
  • Performance is critical
  • Users need smooth scrolling
  • Dataset is too large for regular pagination

❌ Don’t use Virtualization Table when:

  • You have < 1000 rows (use Basic Table)
  • You need variable row heights (virtualization works best with fixed heights)
  • You need complex row interactions (virtualization has limitations)
  1. Fixed height required: Always set a fixed height on DataTable
  2. Consistent row heights: Virtualization works best with uniform row heights
  3. Memoize columns: Wrap column definitions in useMemo
  4. Large page sizes: Use larger page sizes (50-500) for better virtualization