# Virtualisation

> Package `@umriss-ui/table`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/table/virtualisation/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/table/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/table/llms-full.txt).

Thousands of rows the user scrolls through without pages: only the rows in view are rendered, and everything else still reaches every row.

```ts
import { useTable } from "@umriss-ui/table";
```

Demo page: https://romanhaendler.github.io/umriss-ui/table/virtualisation/

## 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.

```tsx
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](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`selectable`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-selectable) | `boolean` | — | Adds the selection column. "All" means: the filtered set. |
| [`stickyHeader`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-stickyHeader) | `boolean` | — | Keeps the header row visible while scrolling. |
| [`stickyRowHeader`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-stickyRowHeader) | `boolean` | — | Keeps the row header column visible while scrolling sideways - the same as `pin="start"` on the row header. |
| [`maxHeight`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-maxHeight) | `string` | — | Height of the scroll area, e.g. "480px". A virtualised table needs it. |
| [`children`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-children) | `ReactNode` | — | Columns, `RowDetail`, `RowActions` and the unbound parts. |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| [`ariaLabel`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-ariaLabel) | `string` | — | Accessible name of the table (not of the outer frame). |

#### Styling

| Prop | Type | Default | Description |
|---|---|---|---|
| [`className`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-className) | `string` | — | Classes on the outer frame of the table. |

From `TableOptions` — the full table stands on [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableOptions).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`rowKey`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableOptions-rowKey) *required* | `(row: Z) => string` | — | A stable key per row – the basis of selection and expansion. |
| [`defaultSort`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableOptions-defaultSort) | [`SortLevel`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-SortLevel)`  \| readonly  `[`SortLevel`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-SortLevel)`[] \| null` | `null` | The application's sort; it is the default that `view` leaves out. |
| [`virtual`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableOptions-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](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0042-a-rows-height-never-follows-what-it-shows.md)). |

From `TableSnapshot` — the full table stands on [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableSnapshot).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`filtered`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-filtered) *required* | `readonly Z[]` | — | The filtered set: what the pre-filter admits and search and filters leave, across all pages. |
| [`visible`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-visible) *required* | `readonly Z[]` | — | What is rendered right now: the page or the window. |
| [`selection`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-selection) *required* | [`TableSelection`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableSelection)`<string>` | — | The selection – its own or the one handed in through `selection`. |

From `FieldColumn` — the full table stands on [Column](https://romanhaendler.github.io/umriss-ui/table/column/#type-FieldColumn).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-value) *required* | `K` | — | A field of the row. Supplies the column's `id` at the same time. |
| [`format`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-format) | [`FormatFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-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`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-children) | [`Presentation`](https://romanhaendler.github.io/umriss-ui/table/column/#type-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](https://romanhaendler.github.io/umriss-ui/table/column/#type-ColumnBase).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`label`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-label) *required* | `string` | — | Names the column in its header, in the column menu and in the header row of the export. |
| [`rowHeader`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-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`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-width) | `number` | the browser's | Initial width in pixels. |

From `SearchProps` — the full table stands on [Search](https://romanhaendler.github.io/umriss-ui/table/search/#type-SearchProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`placeholder`](https://romanhaendler.github.io/umriss-ui/table/search/#SearchProps-placeholder) | `string` | the wording's "Search …" | The placeholder in the field. |

From `ToolbarProps` — the full table stands on [Toolbar](https://romanhaendler.github.io/umriss-ui/table/toolbar/#type-ToolbarProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/toolbar/#ToolbarProps-children) | `ReactNode` | — | Search, column menu, export – on the left in the table toolbar, before the conditions. |

## Known limits

- Paging and virtualisation do not combine; one table does one or the other.
- No loading more rows while scrolling: a table on a server pages ([Server mode](https://romanhaendler.github.io/umriss-ui/table/server-mode/)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
