Virtual Data Table

The portal's virtualized history table: rows scroll inside the table's own frame, with infinite scrollback, live-tail follow, and jump-to-latest.

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-table

Installs this item, every component it depends on, and the npm packages listed below.

Usage

components/virtual-data-table-demo.tsx
"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

PropTypeDescription
data / columns / getRowIdTanStack table inputs; column meta.label names the header.
classNamestringSize the frame here: the table scrolls internally (e.g. h-[360px], or min-h-0 flex-1).
invertedbooleanLog-style: newest at the visual bottom, scrollback above.
onScrollTop / onScrollBottom() => voidInfinite scroll in visual directions; pair with isLoadingTop / isLoadingBottom.
onRowClick / selectedRowIdRow activation and highlight.
streaming / streamStatus / onToggleStreamLive tail: auto-stick at the bottom, a "Streaming · N new" pill when scrolled away, and the tail strip.
streamDroppedCount / onClearStreamDroppedSurface and clear rows the stream dropped.
hasNewer / onLoadNewer / checkForNewerJump-to-latest and background polling when not streaming.
initialScrollToIdstringScroll to an anchor row on mount.
columnVisibility / columnOrder / columnSizingControlled column state (TanStack shapes).
estimateSizenumberRow height estimate for the virtualizer. Default 36.
emptyMessageReactNodeShown when there are no rows.