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.
import { useTable } from "@umriss-ui/table";
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).
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.
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>
);
}
Grid mode keeps everything else: a group header is a line the arrows walk, and in a virtual window the keys reach unrendered rows.
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>
);
}
| 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. |
From TableProps — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
selectable | boolean | — | Adds the selection column. "All" means: the filtered set. |
stickyHeader | boolean | — | Keeps the header row visible while scrolling. |
maxHeight | string | — | Height of the scroll area, e.g. "480px". A virtualised table needs it. |
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 | ReactNode | — | Columns, RowDetail, RowActions and the unbound parts. |
| Name | Type | Default | Description |
|---|---|---|---|
onCellEdit | (edit: 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. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name of the table (not of the outer frame). |
From TableOptions — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
rowKeyrequired | (row: Z) => string | — | A stable key per row – the basis of selection and expansion. |
defaultSort | | SortLevel | null | The application's sort; it is the default that view leaves out. |
defaultGrouping | GroupingId<Z> | readonly 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 | 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). |
From FieldColumn — the full table stands on Column.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | K | — | A field of the row. Supplies the column's id at the same time. |
format | 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 | 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 | (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.
| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the column in its header, in the column menu and in the header row of the export. |
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 | number | the browser's | Initial width in pixels. |
pin | Pin"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.
| Name | Type | Default | Description |
|---|---|---|---|
aggregate | 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.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | The row's Actions. |
From ActionProps — the full table stands on RowActions.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | string | — | The label – text, because it is read out together with the row's name. |
| Name | Type | Default | Description |
|---|---|---|---|
onSelectrequired | ((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. |