Grid mode

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

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.

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.

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.

NameTypeDefaultDescription
selectableboolean—Adds the selection column. "All" means: the filtered set.
stickyHeaderboolean—Keeps the header row visible while scrolling.
maxHeightstring—Height of the scroll area, e.g. "480px". A virtualised table needs it.
gridboolean—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.
childrenReactNode—Columns, RowDetail, RowActions and the unbound parts.

Events

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

Accessibility

NameTypeDefaultDescription
ariaLabelstring—Accessible name of the table (not of the outer frame).

From TableOptions — the full table stands on First table.

NameTypeDefaultDescription
rowKeyrequired(row: Z) => string—A stable key per row – the basis of selection and expansion.
defaultSort| SortLevel
| readonly SortLevel[]
| null
nullThe application's sort; it is the default that view leaves out.
defaultGroupingGroupingId<Z> | readonly GroupingId<Z>[]ungrouped, until the user groupsThe application's grouping: a column or group key, or up to three, the outermost first. It is the default that view leaves out.
virtualboolean | { 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.

NameTypeDefaultDescription
valuerequiredK—A field of the row. Supplies the column's id at the same time.
formatFormatFor<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.
editEditFor<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.

NameTypeDefaultDescription
labelrequiredstring—Names the column in its header, in the column menu and in the header row of the export.
rowHeaderboolean—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.
widthnumberthe browser'sInitial width in pixels.
pinPin
"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.

NameTypeDefaultDescription
aggregateAggregateFor<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.

NameTypeDefaultDescription
childrenrequiredReactNode—The row's Actions.

From ActionProps — the full table stands on RowActions.

NameTypeDefaultDescription
childrenrequiredstring—The label – text, because it is read out together with the row's name.

Events

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

Every page of @umriss-ui/table

Getting started

Columns

Finding rows

Grouping

Rows

Around the table

Many rows

Editing

Limits and alarms

API index