Virtualisation

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";

Examples

Scroll twenty thousand rows

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>
  );
}

Keyboard

Key Action
↑ ↓ Home End On a row: moves to the next, previous, first or last row, rendered or not.

Props on this page

From TableProps — the full table stands on First table.

NameTypeDefaultDescription
selectableboolean—Adds the selection column. "All" means: the filtered set.
stickyHeaderboolean—Keeps the header row visible while scrolling.
stickyRowHeaderboolean—Keeps the row header column visible while scrolling sideways - the same as pin="start" on the row header.
maxHeightstring—Height of the scroll area, e.g. "480px". A virtualised table needs it.
childrenReactNode—Columns, RowDetail, RowActions and the unbound parts.

Accessibility

NameTypeDefaultDescription
ariaLabelstring—Accessible name of the table (not of the outer frame).

Styling

NameTypeDefaultDescription
classNamestring—Classes on the outer frame of the table.

From TableOptions — the full table stands on First table.

NameTypeDefaultDescription
rowKeyrequired(row: Z) => string—A stable key per row – the basis of selection and expansion.
defaultSort| SortLevel
| readonly SortLevel[]
| null
nullThe application's sort; it is the default that view leaves out.
virtualboolean | { 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.

NameTypeDefaultDescription
filteredrequiredreadonly Z[]—The filtered set: what the pre-filter admits and search and filters leave, across all pages.
visiblerequiredreadonly Z[]—What is rendered right now: the page or the window.
selectionrequiredTableSelection<string>—The selection – its own or the one handed in through selection.

From FieldColumn — the full table stands on Column.

NameTypeDefaultDescription
valuerequiredK—A field of the row. Supplies the column's id at the same time.
formatFormatFor<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.
childrenPresentation<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.

NameTypeDefaultDescription
labelrequiredstring—Names the column in its header, in the column menu and in the header row of the export.
rowHeaderboolean—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.
widthnumberthe browser'sInitial width in pixels.

From SearchProps — the full table stands on Search.

NameTypeDefaultDescription
placeholderstringthe wording's "Search …"The placeholder in the field.

From ToolbarProps — the full table stands on Toolbar.

NameTypeDefaultDescription
childrenReactNode—Search, column menu, export – on the left in the table toolbar, before the conditions.

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/table

Getting started

Columns

Finding rows

Grouping

Rows

Around the table

Many rows

Editing

Limits and alarms

API index