Skip to content

Data · Virtualised table

Virtualised table

Opt-in virtualisation for the case where the DOM, not the data, is the bottleneck. The visible rows are still real table rows — two hidden spacer rows preserve the layout — so semantics never degrade into absolutely-positioned divs.

Examples

import { DataTablePagination, DataTableToolbar, StatCard, VirtualDataTable } from "@luc/ui";
Rows loaded
10,000
Rows in the DOM
0
Window plus overscan
Rendered rows
0
of 10,000 loaded

10,000 fills

Scroll, sort and filter the whole set. Only the window plus a small overscan is mounted, so interaction stays responsive without pretending this is a benchmark.

Trade ledger
Side

1–10000 of 10000 fills

Rows per page
1 / 1

When to virtualise

Virtualising twenty rows costs correctness and buys nothing.

Use DataTable

  • Under a few hundred rows on one page.
  • When browser find-in-page and text selection matter most.
  • Anywhere row expansion or variable row heights are used.

Use VirtualDataTable

  • Thousands of rows in one scroll area.
  • Fixed row height and no expansion.
  • When mounting every row measurably slows interaction.
<VirtualDataTable columns={columns} data={rows} // 10,000 rows getRowId={(row) => row.id} height={520} rowHeight={40} // must match the rendered row overscan={12} stickyHeader ariaLabel="Trade ledger" />

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

Exported symbols
ExportKindSignature
VirtualDataTable52 documented props<TData extends RowData>({ ariaLabel, ariaDescription, caption, state: tableState, emptyState, errorState, density, rowHeight, overscan, height, stickyHeader, showColumnBorders, toolbar, footer, onRow…

VirtualDataTable props

Type: VirtualDataTableProps<TData>

Values the compiler reads from the declaration.

VirtualDataTable props
PropTypeDefaultNotes
ariaDescription?string | undefined—
ariaLabelstring—
caption?ReactNode—
className?string | undefined—
columnFilters?ColumnFiltersState | undefined—
columnPinning?ColumnPinningState | undefined—
columnsreadonly DataTableColumn<TData>[]—
columnVisibility?ColumnVisibilityState | undefined—
dataTData[]—
defaultColumnFilters?ColumnFiltersState | undefined—
defaultColumnPinning?ColumnPinningState | undefined—
defaultColumnVisibility?ColumnVisibilityState | undefined—
defaultGlobalFilter?string | undefined—
defaultPagination?PaginationState | undefined—
defaultRowSelection?RowSelectionState | undefined—
defaultSorting?SortingState | undefined—
density?DataTableDensity | undefined—
emptyState?ReactNode—
enableColumnVisibility?boolean | undefined—
enableFiltering?boolean | undefined—
enableGlobalFilter?boolean | undefined—
enableRowSelection?boolean | "single" | undefined—Adds a leading checkbox column. `"single"` restricts to one row.
enableSorting?boolean | undefined—
errorState?ReactNode—
footer?ReactNode—
getRowId?((row: TData, index: number) => string) | undefined—Stable row identity. Required for selection to survive sorting/filtering.
globalFilter?string | undefined—
height?string | number | undefined—Viewport height of the scroll area. Default `32rem`.
manualFiltering?boolean | undefined—
manualPagination?boolean | undefined—Server-side modes: the table stops slicing and expects prepared data.
manualSorting?boolean | undefined—
onColumnFiltersChange?((filters: ColumnFiltersState) => void) | undefined—
onColumnPinningChange?((pinning: ColumnPinningState) => void) | undefined—
onColumnVisibilityChange?((visibility: ColumnVisibilityState) => void) | undefined—
onGlobalFilterChange?((value: string) => void) | undefined—
onPaginationChange?((pagination: PaginationState) => void) | undefined—
onRenderedRowCountChange?((count: number) => void) | undefined—Reports how many rows are actually in the DOM, for honest status text.
onRowClick?((row: TData) => void) | undefined—
onRowSelectionChange?((selection: RowSelectionState) => void) | undefined—
onSortingChange?((sorting: SortingState) => void) | undefined—
overscan?number | undefined—Rows rendered beyond the viewport. 12 is smooth without wasting frames.
pageCount?number | undefined—Total pages / rows known to the server.
pageSize?number | undefined—Default page size when the caller does not control pagination.
pagination?PaginationState | undefined—
rowCount?number | undefined—
rowHeight?number | undefined—Row height in pixels. Must match the rendered row for correct scrolling.
rowSelection?RowSelectionState | undefined—
showColumnBorders?boolean | undefined—
sorting?SortingState | undefined—
state?DataTableState | undefined—
stickyHeader?boolean | undefined—Sticky headers are the point of a virtual table; on by default.
toolbar?ReactNode—

Usage

import { VirtualDataTable } from "@luc/ui/data-table";
<VirtualDataTable
  columns={columns}
  data={rows}
  ariaLabel="All 12,480 trades"
  height={520}
/>

Guidelines

Do

  • Reach for it above a few hundred rows, or when the table sits inside a fixed-height panel and the page must not grow.
  • Keep `ariaLabel` descriptive of the whole dataset, because only the visible rows are in the DOM.

Don't

  • Virtualise a table whose rows must all be reachable by browser find-in-page or by a print stylesheet; the DOM only holds the window.

Accessibility

The header stays a real header row while the body is windowed, and the caption names the full dataset so the row count does not read as the DOM count.

Luc UI v1.0VirtualDataTabledark theme