Components
Registry
Components
Time
Route
Status
Duration
"use client"
import { useMemo, useState } from "react"Rows render through @tanstack/react-virtual inside the table's own scroll
container: the page never scrolls on the table's behalf, and five thousand
rows cost the same as fifty. The column header stays fixed above the scroll
area. Everything stateful is driven by callbacks, so any data source works.
Set inverted for log-style feeds: newest rows sit at the visual bottom,
scrollback loads upward, and the bottom strip becomes the live-tail control
(with Ctrl+Space) when onToggleStream is wired.
Installation#
pnpm dlx shadcn@latest add @repost/virtual-data-tableInstalls this item, every component it depends on, and the npm packages listed below.
Usage#
"use client"
import { useMemo, useState } from "react"
import type { ColumnDef } from "@tanstack/react-table"
import { VirtualDataTable } from "@/components/ui/virtual-data-table"
import { StatusCodeBadge } from "@/components/ui/status-code-badge"
type DemoRow = {
id: string
time: string
route: string
status: number
durationMs: number
}
const ROUTES = ["/v1/messages", "/v1/endpoints", "/v1/logs", "/v1/replays", "/v1/events"]
const BASE_TIME = Date.parse("2026-07-30T12:00:00.000Z")
/** 5,000 deterministic rows — only the visible ones render. */
const buildRows = (): DemoRow[] =>
Array.from({ length: 5000 }, (_, index) => ({
id: `req_${index}`,
time: new Date(BASE_TIME - index * 11_000).toLocaleTimeString(),
route: ROUTES[index % ROUTES.length]!,
status: index % 23 === 7 ? 429 : index % 17 === 3 ? 500 : 200,
durationMs: 12 + ((index * 37) % 240),
}))
export function VirtualDataTableDemo() {
const [selected, setSelected] = useState<string | null>(null)
const rows = useMemo(() => buildRows(), [])
const columns = useMemo<ColumnDef<DemoRow, unknown>[]>(
() => [
{
id: "time",
meta: { label: "Time" },
size: 120,
cell: ({ row }) => <span className="font-mono text-muted-foreground tabular-nums">{row.original.time}</span>,
},
{
id: "route",
meta: { label: "Route" },
size: 220,
cell: ({ row }) => <span className="truncate font-medium">{row.original.route}</span>,
},
{
id: "status",
meta: { label: "Status" },
size: 90,
cell: ({ row }) => <StatusCodeBadge statusCode={row.original.status} />,
},
{
id: "duration",
meta: { label: "Duration" },
size: 100,
cell: ({ row }) => <span className="font-mono text-muted-foreground tabular-nums">{row.original.durationMs}ms</span>,
},
],
[]
)
return (
<VirtualDataTable
className="h-[360px] w-full"
data={rows}
columns={columns}
getRowId={(row) => row.id}
onRowClick={(row) => setSelected((current) => (current === row.id ? null : row.id))}
selectedRowId={selected}
fillColumnId="route"
rounded
borderX
/>
)
}
Props#
| Prop | Type | Description |
|---|---|---|
data / columns / getRowId | — | TanStack table inputs; column meta.label names the header. |
className | string | Size the frame here: the table scrolls internally (e.g. h-[360px], or min-h-0 flex-1). |
inverted | boolean | Log-style: newest at the visual bottom, scrollback above. |
onScrollTop / onScrollBottom | () => void | Infinite scroll in visual directions; pair with isLoadingTop / isLoadingBottom. |
onRowClick / selectedRowId | — | Row activation and highlight. |
streaming / streamStatus / onToggleStream | — | Live tail: auto-stick at the bottom, a "Streaming · N new" pill when scrolled away, and the tail strip. |
streamDroppedCount / onClearStreamDropped | — | Surface and clear rows the stream dropped. |
hasNewer / onLoadNewer / checkForNewer | — | Jump-to-latest and background polling when not streaming. |
initialScrollToId | string | Scroll to an anchor row on mount. |
columnVisibility / columnOrder / columnSizing | — | Controlled column state (TanStack shapes). |
estimateSize | number | Row height estimate for the virtualizer. Default 36. |
emptyMessage | ReactNode | Shown when there are no rows. |