# Grid mode

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

Make the table one Tab stop whose cells the arrow keys walk, as in a spreadsheet. It is the mode for editing; keep it off for tables that are only read.

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

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

A table is read by default: a native table, where every control is one Tab away and a screen reader keeps its table keys. Grid mode changes that on request only ([ADR-0034](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0034-the-table-becomes-a-grid-only-on-request.md)).

## Examples

### Work cell by cell

`grid` makes the table one Tab stop whose cells the arrows walk; Enter reaches a cell's controls. Keep it off for tables that are only read.

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

interface Service {
  id: string;
  name: string;
  team: string;
  requests: number;
  state: string;
}

const SERVICES: Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", requests: 1840, state: "Degraded" },
  { id: "billing", name: "Billing", team: "Payments", requests: 620, state: "Healthy" },
  { id: "sign-in", name: "Sign-in", team: "Identity", requests: 2210, state: "Healthy" },
  { id: "search", name: "Search", team: "Discovery", requests: 3105, state: "Healthy" },
  { id: "webhooks", name: "Webhooks", team: "Integrations", requests: 410, state: "Delayed" },
];

export default function GridMode() {
  const { Table, Column, RowActions, Action } = useTable(SERVICES, { rowKey: (s) => s.id });
  return (
    <Table grid selectable ariaLabel="Services">
      <Column value="name" label="Service" rowHeader />
      <Column value="team" label="Team" />
      <Column value="requests" label="Requests a minute" format="count" aggregate="sum" />
      <Column value="state" label="State" />
      <RowActions>
        <Action onSelect={() => undefined}>Dashboard</Action>
      </RowActions>
    </Table>
  );
}
```

### Combine grid mode with groups, pinning and many rows

Grid mode keeps everything else: a group header is a line the arrows walk, and in a virtual window the keys reach unrendered rows.

```tsx
import { useMemo, useState } from "react";
import { useTable } from "@umriss-ui/table";

interface Parcel {
  id: string;
  parcel: string;
  depot: string;
  weight: number;
  declared: number;
  note: string;
}

const DEPOTS = ["North depot", "Riverside depot", "East Gate depot"] as const;

/* A linear congruential generator: the same seed yields the same rows. */
function generate(count: number): Parcel[] {
  let seed = 20260924;
  const next = () => {
    seed = (seed * 1103515245 + 12345) % 2147483648;
    return seed / 2147483648;
  };
  return Array.from({ length: count }, (_, i) => ({
    id: `x${i}`,
    parcel: `FP-${String(1_004_210 + i)}`,
    depot: DEPOTS[Math.floor(next() * DEPOTS.length)]!,
    weight: Math.round(next() * 300) / 10,
    declared: Math.round(next() * 50_000) / 100,
    note: "",
  }));
}

export default function TheWholeGrid() {
  const generated = useMemo(() => generate(2000), []);
  const [parcels, setParcels] = useState(generated);
  const { Table, Column } = useTable(parcels, {
    rowKey: (p) => p.id,
    defaultGrouping: "depot",
    defaultSort: { column: "parcel", direction: "asc" },
    virtual: true,
  });
  return (
    <Table
      grid
      selectable
      stickyHeader
      maxHeight="360px"
      ariaLabel="Parcels by depot"
      onCellEdit={({ rowKey, columnId, value }) =>
        setParcels((all) => all.map((p) => (p.id === rowKey ? { ...p, [columnId]: value } : p)))
      }
    >
      <Column value="parcel" label="Parcel" rowHeader pin="start" width={130} />
      <Column value="depot" label="Depot" />
      <Column
        value="weight"
        label="Weight (kg)"
        format={{ decimals: 1 }}
        edit="number"
        validate={(value) => (value === null || value <= 0 || value > 31.5 ? "Between 0 and 31.5 kg" : undefined)}
        width={140}
      />
      <Column value="declared" label="Declared value (€)" format={{ decimals: 2 }} aggregate="sum" width={170} />
      <Column value="note" label="Note" edit="text" width={260} />
    </Table>
  );
}
```

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Enters and leaves the grid, which is one stop. |
| `← → ↑ ↓` | Moves the active cell. |
| `Home / End` | The first or last cell of the row. |
| `Ctrl + Home / Ctrl + End` | The first cell of the head, the last cell of the footer. |
| `Page Up / Page Down` | Moves by the rows in view. |
| `Click` | Opens the editor of a cell that edits; a select shows its list, a day its calendar. |
| `Enter / F2` | Starts an edit, or reaches the cell's own controls. |
| `Escape` | Cancels an edit, or goes back from a control to its cell. |
| `Ctrl / Cmd + C` | Copies the selected rows, or the active cell; its cell flashes. In an open editor with nothing marked it copies the field's value. |
| `Space` | Selects the row of the active cell. |

## 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. |
| [`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. |
| [`grid`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-grid) | `boolean` | — | Grid mode: the table is one tab stop, and the arrows walk its cells - Home and End in the row, Ctrl+Home and Ctrl+End to the table's ends, PageUp and PageDown by the rows in view. A cell's own controls are reached with Enter or F2 and left with Escape; a column with `edit` is edited in place. Without it the table stays a native table. |
| [`children`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-children) | `ReactNode` | — | Columns, `RowDetail`, `RowActions` and the unbound parts. |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| [`onCellEdit`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-onCellEdit) | `(edit: `[`CellEdit`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-CellEdit)`<Z>) => void` | — | Grid mode: an edit the user committed. The table applies nothing; the cell shows the new value when the rows passed in carry it. |

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

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. |
| [`defaultGrouping`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableOptions-defaultGrouping) | [`GroupingId`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-GroupingId)`<Z> \| readonly `[`GroupingId`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-GroupingId)`<Z>[]` | ungrouped, until the user groups | The application's grouping: a column or group key, or up to three, the outermost first. 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 `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. |
| [`edit`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-edit) | [`EditFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-EditFor)`<Z[K], Z>` | — | Edits the column's cells in place in grid mode: Enter, F2 or typing starts it, Enter or Tab commits, Escape cancels. The table applies nothing - the edit is reported through the table's `onCellEdit`. From `EditOptions`. |
| [`validate`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-validate) | `(value: Z[K] \| null, row: Z) => string \| null \| undefined` | — | Checks a draft before it is reported: a message keeps the editor open and stands beneath the field, as in a `FormField`; nothing lets it through. From `EditOptions`. |

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. |
| [`pin`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-pin) | [`Pin`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Pin)<br>`"start" \| "end"` | — | Keeps the column in view while the table scrolls sideways: in a block before every other column, or after them. The user can change it in the column menu; the view carries the change. Pinned blocks take at most half of the width the table shows: on a narrower one the end block, then the start block scrolls with the rest, and sticks again when there is room. |

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`aggregate`](https://romanhaendler.github.io/umriss-ui/table/aggregate/#AggregateOptions-aggregate) | [`AggregateFor`](https://romanhaendler.github.io/umriss-ui/table/aggregate/#type-AggregateFor)`<W, Z>` | — | What the column's values come to: in the footer over the filtered set, in a group's header over its rows. `"sum"`, `"avg"` for numbers; `"min"`, `"max"` for numbers and points in time; `"range"` for points in time; `"count"`, `"distinct"` for every value with a text form - or a function of one's own. Always from the values, never from other aggregates; absent values count towards nothing. |

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/rowactions/#RowActionsProps-children) *required* | `ReactNode` | — | The row's `Action`s. |

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/rowactions/#ActionProps-children) *required* | `string` | — | The label – text, because it is read out together with the row's name. |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| [`onSelect`](https://romanhaendler.github.io/umriss-ui/table/rowactions/#ActionProps-onSelect) *required* | `((row: Z) => void) \| ((rows: readonly Z[]) => void)` | — | Gets the row the action was triggered on. With `bulk` a list: at the row one made from it, in the table toolbar the selection. |
