The bar under the table that splits long results into pages, with the page size to choose (also called a pager). The table pages only where one stands; without it every filtered row is shown.
import { Pagination } from "@umriss-ui/table";
pageSize on the hook sets the rows per page, pageSizes the choices. A search, a filter, a new sort or a new page size go back to page one, and a page that no longer exists after a filter becomes the last one. A virtualised table does not page, and the bar is not drawn.
Put a Pagination in the table and set pageSize on the hook; a search, a filter or a new sort goes back to page one.
import { Pagination, Search, Toolbar, useTable } from "@umriss-ui/table";
/* Data from the logistics world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(10, 30);
interface Vehicle {
id: string;
plate: string;
type: "van" | "e-van" | "truck";
/** Payload, in kg. */
capacity: number;
depot: string;
}
const VEHICLES: readonly Vehicle[] = [
{ id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
{ id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
{ id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
{ id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
{ id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
{ id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
{ id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
{ id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
];
const DRIVERS = [
{ id: "d1", name: "Martin Hale", depot: "north" },
{ id: "d2", name: "Nadia Petrova", depot: "north" },
{ id: "d3", name: "Owen Carter", depot: "north" },
{ id: "d4", name: "Lucia Romero", depot: "river" },
{ id: "d5", name: "Ben Adeyemi", depot: "river" },
{ id: "d6", name: "Hanna Berg", depot: "east" },
{ id: "d7", name: "Yusuf Demir", depot: "east" },
{ id: "d8", name: "Clara Wendt", depot: "east" },
] as const;
const CUSTOMERS = [
"Holloway Garden Supplies",
"Marlow & Finch Books",
"Oakridge Pharmacy",
"Brixley Cycles",
"Tamsin's Bakery",
"Northfold Office",
"Pellham Hardware",
"Greywick Studio",
"Juniper Lane Florist",
"Ashcombe Dental",
] as const;
interface Stop {
id: string;
shipment: string;
customer: string;
/** The window the customer was promised, `[from, to]`. */
window: readonly [number, number];
/** When the tour plans to be there. */
arrival: number;
}
interface Tour {
id: string;
vehicle: string;
driver: string;
depot: string;
/** Leaves the depot, loaded; back at the depot. */
from: number;
to: number;
/** Minutes of loading before `from`. */
loading: number;
km: number;
stops: readonly Stop[];
}
/** One tour per vehicle. Stops every half hour or so, each with a two-hour
window; every seventh arrival misses its window - the late ones a
dispatcher looks for. */
function tour(index: number): Tour {
const r = random(700 + index);
const vehicle = VEHICLES[index]!;
const truck = vehicle.type === "truck";
const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
const count = truck ? 5 : 8 + Math.floor(r() * 4);
const stops: Stop[] = [];
let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
for (let i = 0; i < count; i++) {
const n = index * 20 + i;
const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
/* A late stop's window closed half an hour or more before the arrival. */
const late = i >= 2 && n % 7 === 3;
const halfHour = 30 * MINUTE;
const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
stops.push({
id: `s-${index + 1}-${i + 1}`,
shipment: `FP-${(1_004_210 + n * 13).toString()}`,
customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
arrival,
});
arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
}
return {
id: `T-${String(index + 1).padStart(2, "0")}`,
vehicle: vehicle.id,
driver: DRIVERS[index]!.id,
depot: vehicle.depot,
from,
to: arrival + 30 * MINUTE,
loading: truck ? 45 : 30,
km: Math.round((truck ? 140 : 70) + r() * 40),
stops,
};
}
const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
interface Shipment {
id: string;
customer: string;
/** In kg. */
weight: number;
status: "delivered" | "out for delivery" | "failed attempt";
window: readonly [number, number];
tour: string;
}
/** One shipment per stop. Before now, delivered - or a failed attempt, where
nobody was in; after now, out for delivery. */
const SHIPMENTS: readonly Shipment[] = TOURS.flatMap((one) =>
one.stops.map((stop) => {
const n = Number(stop.shipment.slice(3));
return {
id: stop.shipment,
customer: stop.customer,
weight: Math.round((VEHICLES.find((v) => v.id === one.vehicle)!.type === "truck" ? 180 : 2) + ((n * 7919) % 230) / 10),
status: stop.arrival > NOW ? "out for delivery" : n % 11 === 0 ? "failed attempt" : "delivered",
window: stop.window,
tour: one.id,
} satisfies Shipment;
}),
);
export default function PageThroughShipments() {
const { Table, Column } = useTable(SHIPMENTS, { rowKey: (s) => s.id, pageSize: 10 });
return (
<Table ariaLabel="Today's shipments">
<Toolbar>
<Search placeholder="Search customer" />
</Toolbar>
<Column value="id" label="Shipment" rowHeader />
<Column value="customer" label="Customer" />
<Column value="status" label="Status" />
<Column value="weight" label="Weight (kg)" />
<Pagination pageSizes={[10, 25, 50]} />
</Table>
);
}
Without a Pagination the table does not page at all – here twelve rows stand despite pageSize: 5 on the hook.
import { useTable } from "@umriss-ui/table";
interface Delivery {
id: string;
parcels: number;
}
const DELIVERIES: Delivery[] = Array.from({ length: 12 }, (_, i) => ({
id: `FP-${1004300 + i * 13}`,
parcels: 1 + ((i * 5) % 7),
}));
export default function ShowEveryRow() {
const { Table, Column } = useTable(DELIVERIES, { rowKey: (d) => d.id, pageSize: 5 });
return (
<Table density="compact" ariaLabel="Deliveries without a paging bar">
<Column value="id" label="Shipment" rowHeader />
<Column value="parcels" label="Parcels" />
</Table>
);
}
The keys of First table, Input, Select and Button apply here.
PaginationProps| Name | Type | Default | Description |
|---|---|---|---|
pageSizes | readonly number[] | [10, 25, 50] | The selectable page sizes. Shown in: Page through shipments, Search and page from a header of your own (First table), Restrict which rows a table has (Pre-filter) and 4 more |
of | TableRef | — | The table, when the part does not stand inside it: the return value of useTable.Shown in: Parts outside a toolbar (Toolbar) |
| Name | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes on the pagination bar. |
TableRefWhat a part without a row type needs from a table – for of.
type TableRef = Omit<TableSnapshot<unknown>, "setFilter" | "conditionOf"> & {
readonly Table: unknown;
/** Without a row kind: any id, any condition. */
setFilter(column: string, condition: unknown): void;
};rowCount decides how many pages there are.What umriss deliberately does not build, and why: ADR-0032.