# Pagination

> Package `@umriss-ui/table`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/table/pagination/>. 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).

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.

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

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

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

## Examples

### Page through shipments

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.

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

### Show every row

Without a `Pagination` the table does not page at all – here twelve rows stand despite `pageSize: 5` on the hook.

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

## When to use something else

- Thousands of rows the user scrolls through without pages → Virtualisation

## Keyboard

The keys of [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#keyboard-first-table), [Input](https://romanhaendler.github.io/umriss-ui/core/input/#keyboard-input), [Select](https://romanhaendler.github.io/umriss-ui/core/select/#keyboard-select) and [Button](https://romanhaendler.github.io/umriss-ui/core/button/#keyboard-button) apply here.

## API

### `PaginationProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `pageSizes` | `readonly number[]` | `[10, 25, 50]` | The selectable page sizes.<br>Shown in: [Page through shipments](https://romanhaendler.github.io/umriss-ui/table/pagination/#page-through-shipments), [Search and page from a header of your own (First table)](https://romanhaendler.github.io/umriss-ui/table/first-table/#search-and-page-from-a-header-of-your-own), [Restrict which rows a table has (Pre-filter)](https://romanhaendler.github.io/umriss-ui/table/pre-filter/#pre-filter) and 4 more |
| `of` | [`TableRef`](https://romanhaendler.github.io/umriss-ui/table/pagination/#type-TableRef) | — | The table, when the part does not stand inside it: the return value of `useTable`.<br>Shown in: [Parts outside a toolbar (Toolbar)](https://romanhaendler.github.io/umriss-ui/table/toolbar/#parts-outside-a-toolbar) |

#### Styling

| Prop | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Classes on the pagination bar. |

### Types on this page

#### `TableRef`

What a part without a row type needs from a table – for `of`.

```ts
type TableRef = Omit<TableSnapshot<unknown>, "setFilter" | "conditionOf"> & {
  readonly Table: unknown;
  /** Without a row kind: any id, any condition. */
  setFilter(column: string, condition: unknown): void;
};
```

## Known limits

- In [server mode](https://romanhaendler.github.io/umriss-ui/table/server-mode/) the page goes to the server in the request, and `rowCount` decides how many pages there are.

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