Thousands of rows the user scrolls through without pages: only the rows in view are rendered, and everything else still reaches every row.
import { useTable } from "@umriss-ui/table";
virtual renders only the rows in view; sorting, search, “select all” and the arrow keys still reach every row. It replaces paging.
import { useMemo } from "react";
import { Badge, Stack, Text, useFormats } from "@umriss-ui/core";
import { Search, Toolbar, useTable } from "@umriss-ui/table";
/* The minimum width comes from the application's stylesheet - `width` is an
initial width - so that header and row header have something to stick
against sideways as well. */
const STYLE = `
.request-log table {
min-width: 1300px;
}
`;
interface Request {
id: string;
service: string;
route: string;
status: 200 | 404 | 500;
duration: number;
size: number;
region: string;
client: string;
}
const SERVICES = ["Checkout", "Billing", "Sign-in", "Search", "Image service", "Notifications", "Webhooks", "Reporting"] as const;
const ROUTES = ["/api/cart", "/api/pay", "/api/session", "/api/search", "/img/thumb", "/api/notify", "/hooks/deliver", "/api/report"] as const;
const REGIONS = ["eu-west", "eu-central", "us-east", "ap-south"] as const;
const CLIENTS = ["Web shop", "iOS app", "Android app", "Partner API"] as const;
const STATUSES = [200, 200, 200, 200, 200, 200, 404, 500] as const;
const TONE = { 200: "success", 404: "warning", 500: "danger" } as const;
const pick = <T,>(field: readonly T[], share: number): T => field[Math.floor(share * field.length) % field.length]!;
/* A linear congruential generator: the same seed yields the same rows. */
function generate(count: number): Request[] {
let seed = 20260317;
const next = () => {
seed = (seed * 1103515245 + 12345) % 2147483648;
return seed / 2147483648;
};
return Array.from({ length: count }, (_, i) => {
const service = Math.floor(next() * SERVICES.length);
return {
id: `REQ-${String(i + 1).padStart(5, "0")}`,
service: SERVICES[service]!,
route: ROUTES[service]!,
status: pick(STATUSES, next()),
duration: Math.round(20 + next() * 900),
size: Math.round(next() * 5000) / 10,
region: pick(REGIONS, next()),
client: pick(CLIENTS, next()),
};
});
}
export default function Virtualisation() {
const requests = useMemo(() => generate(20_000), []);
const formats = useFormats();
const t = useTable(requests, {
rowKey: (r) => r.id,
defaultSort: { column: "id", direction: "asc" },
virtual: true,
});
const { Table, Column } = t;
return (
<Stack gap={2}>
<style>{STYLE}</style>
<Table className="request-log" selectable stickyHeader stickyRowHeader maxHeight="340px" ariaLabel="Requests">
<Toolbar>
<Search placeholder="Request or service" />
</Toolbar>
<Column value="id" label="Request" rowHeader width={130} />
<Column value="service" label="Service" width={140} />
<Column value="route" label="Route" width={150} />
<Column value="status" label="Status" width={100}>
{(status) => <Badge tone={TONE[status]}>{status}</Badge>}
</Column>
<Column value="duration" label="Duration (ms)" width={130} />
<Column value="size" label="Size (kB)" format={{ decimals: 1 }} width={110} />
<Column value="region" label="Region" width={120} />
<Column value="client" label="Client" width={140} />
</Table>
<Text size="xs" tone="muted" mono>
{t.visible.length} of {formats.count(t.filtered.length)} rendered · {t.selection.count} selected
</Text>
</Stack>
);
}
| Key | Action |
|---|---|
↑ ↓ Home End |
On a row: moves to the next, previous, first or last row, rendered or not. |
From TableProps — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
selectable | boolean | — | Adds the selection column. "All" means: the filtered set. |
stickyHeader | boolean | — | Keeps the header row visible while scrolling. |
stickyRowHeader | boolean | — | Keeps the row header column visible while scrolling sideways - the same as
pin="start" on the row header. |
maxHeight | string | — | Height of the scroll area, e.g. "480px". A virtualised table needs it. |
children | ReactNode | — | Columns, RowDetail, RowActions and the unbound parts. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name of the table (not of the outer frame). |
| Name | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes on the outer frame of the table. |
From TableOptions — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
rowKeyrequired | (row: Z) => string | — | A stable key per row – the basis of selection and expansion. |
defaultSort | | SortLevel | null | The application's sort; it is the default that view leaves out. |
virtual | boolean | { overscan?: number } | — | Renders only what stands in the scroll area; paging is then off. Every row is one row pitch tall, which the table measures itself (ADR-0042). |
From TableSnapshot — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
filteredrequired | readonly Z[] | — | The filtered set: what the pre-filter admits and search and filters leave, across all pages. |
visiblerequired | readonly Z[] | — | What is rendered right now: the page or the window. |
selectionrequired | TableSelection<string> | — | The selection – its own or the one handed in through selection. |
From FieldColumn — the full table stands on Column.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | K | — | A field of the row. Supplies the column's id at the same time. |
format | FormatFor<Z[K]> | — | A standard presentation by name: "percent", "count" or
{ decimals } for numbers, "date", "time" or "dateTime" for
points in time. Alignment, sorting, export and footer stay with the value. |
children | Presentation<Z[K], Z> | — | How the value appears: (value, row). Never called for an absent
value. Required where the value has no text form. |
From ColumnBase — the full table stands on Column.
| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the column in its header, in the column menu and in the header row of the export. |
rowHeader | boolean | — | This column names the row: row header cells, never hideable, the column
stickyRowHeader pins, and the name by which the row's selection, expander and
actions are read out. At most one per table. |
width | number | the browser's | Initial width in pixels. |
From SearchProps — the full table stands on Search.
| Name | Type | Default | Description |
|---|---|---|---|
placeholder | string | the wording's "Search …" | The placeholder in the field. |
What umriss deliberately does not build, and why: ADR-0032.