Data table: Virtual rows
For long lists, pass virtual to the data table with the virtualizer hook as use: it then renders only the rows in view, inside a scroll area with a sticky header. The hook is useVirtualizer from @tanstack/react-virtual.
| Message | ||||
|---|---|---|---|---|
| 1 | 00:00:00 | info | api | Request served #1 |
| 2 | 00:00:07 | info | search | Cache refreshed #2 |
| 3 | 00:00:14 | error | auth | Token renewed #3 |
| 4 | 00:00:21 | info | worker | Job queued #4 |
| 5 | 00:00:28 | warn | billing | Retrying upstream call #5 |
| 6 | 00:00:35 | info | api | Request served #6 |
| 7 | 00:00:42 | info | search | Cache refreshed #7 |
| 8 | 00:00:49 | error | auth | Token renewed #8 |
| 9 | 00:00:56 | info | worker | Job queued #9 |
| 10 | 00:01:03 | warn | billing | Retrying upstream call #10 |
| 11 | 00:01:10 | info | api | Request served #11 |
| 12 | 00:01:17 | info | search | Cache refreshed #12 |
| 13 | 00:01:24 | error | auth | Token renewed #13 |
| 14 | 00:01:31 | info | worker | Job queued #14 |
| 15 | 00:01:38 | warn | billing | Retrying upstream call #15 |
| 16 | 00:01:45 | info | api | Request served #16 |
| 17 | 00:01:52 | info | search | Cache refreshed #17 |
| 18 | 00:01:59 | error | auth | Token renewed #18 |
| 19 | 00:02:06 | info | worker | Job queued #19 |
import { useVirtualizer } from "@tanstack/react-virtual";
import { DataTable, type DataTableColumnDef } from "slotsmith/data-table";
type LogLine = { id: number; time: string; level: string; service: string; message: string };
const LEVELS = ["info", "info", "info", "warn", "error"];
const SERVICES = ["api", "auth", "billing", "search", "worker"];
const MESSAGES = ["Request served", "Cache refreshed", "Token renewed", "Job queued", "Retrying upstream call"];
/** 10,000 log lines, the same on every render: line i is computed from i alone. */
const lines: LogLine[] = Array.from({ length: 10_000 }, (_, i) => {
const seconds = i * 7;
const time = [Math.floor(seconds / 3600) % 24, Math.floor(seconds / 60) % 60, seconds % 60]
.map((part) => String(part).padStart(2, "0"))
.join(":");
return {
id: i + 1,
time,
level: LEVELS[(i * 7) % LEVELS.length]!,
service: SERVICES[(i * 3) % SERVICES.length]!,
message: `${MESSAGES[(i * 11) % MESSAGES.length]} #${i + 1}`,
};
});
const columns: DataTableColumnDef<LogLine>[] = [
{ accessorKey: "id", header: "#", meta: { align: "end" } },
{ accessorKey: "time", header: "Time" },
{ accessorKey: "level", header: "Level" },
{ accessorKey: "service", header: "Service" },
{ accessorKey: "message", header: "Message", enableSorting: false },
];
export default function VirtualRows() {
return (
<DataTable
data={lines}
columns={columns}
getRowId={(line) => String(line.id)}
// Only the rows in view are rendered, inside a scroll area this tall.
// Rows are not measured: estimateSize is their height, so keep them on one line.
virtual={{ use: useVirtualizer, estimateSize: 40, maxHeight: 360 }}
slotProps={{ cell: () => ({ style: { whiteSpace: "nowrap" } }) }}
/>
);
}
The table holds 10,000 log lines, built from their index so every render gets the same ones. Scroll it, or sort by a column: only the rows on screen exist in the page.
Install
@tanstack/react-virtual is an optional peer: you pass its hook in, so slotsmith itself never imports it, and a project that never windows a list does not need it.
pnpm add @tanstack/react-virtual
The virtual option
virtual takes the hook as use, and three numbers:
maxHeight: the height of the scroll area, in pixels or as a CSS length. Defaults to 480.estimateSize: the height of one row, in pixels. Defaults to 40. Rows are not measured, so set it to the height your rows have, and keep them that height: the demo stops its cells from wrapping withslotProps.cell, so a narrow screen scrolls the table sideways instead of growing the rows.overscan: how many rows to render beyond the visible ones, so a fast scroll shows no blank space. Defaults to 10.
Pagination is off by default; enablePagination turns it back on. The table is compact (size="sm") unless you pass size="default".
Everything else works as in DataTable: sorting, selection, tree rows, the loading, empty and error states, labels and every slot. Rows above and below the window are stood in for by empty spacer rows, so the native table layout, and every Row and Cell slot, stays as it is.
Row reorder and spreadsheet mode work too. A dragged row stays rendered while it is scrolled out of the window, and the view scrolls under it near the edges; the keyboard scrolls to the row it moves to.
VirtualDataTable from slotsmith/virtual is the same table with useVirtualizer already passed in, for code written before virtual took the hook.
Your own layout
With virtual, DataTable is DataTable.Provider with a virtual body. To place the parts yourself, render DataTable.VirtualBody as the body of DataTable.Table, with the hook as use. Keep the scroll area in state with the table’s scrollRef (a callback ref such as a state setter), and pass that element to the body as scrollElement.