# Column

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

One value per row, read from a field or computed. Everything the table does with a column (sort, search, export, total) works on that value, never on what the cell shows.

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

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

Without a presentation the value type decides: text on the left, numbers right-aligned with tabular figures, points in time as date and time, truth values as a word.

A computed value needs an `id`. One column is the row header: it names the row, cannot be hidden, and the row's checkbox and actions are read out with it.

A computed `value` (like `sortValue`, `groupValue` and `rowKey`) may be written inline: a new function with the same source text counts as the same one, and nothing is recomputed. A closure that reads an outer variable which has changed reads as unchanged; give the function a new text, or carry the variable in the row data.

## Examples

### Read a value from a field

`value` names a field of the row, and the compiler offers only the fields there are; the field name is the column's id as well.

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

interface Vehicle {
  plate: string;
  type: string;
  capacity: number;
}

const VEHICLES: Vehicle[] = [
  { plate: "FP 214 K", type: "van", capacity: 1200 },
  { plate: "FP 377 K", type: "e-van", capacity: 900 },
  { plate: "FP 118 R", type: "truck", capacity: 7500 },
];

export default function ValueFromField() {
  const { Table, Column } = useTable(VEHICLES, { rowKey: (v) => v.plate });

  return (
    <Table ariaLabel="Vehicles">
      <Column value="plate" label="Plate" rowHeader />
      <Column value="type" label="Type" />
      <Column value="capacity" label="Payload (kg)" />
    </Table>
  );
}
```

### Compute a value from the row

When `value` is a function the column needs an `id`; the computed value sorts, formats and exports like any other.

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

interface Line {
  description: string;
  quantity: number;
  unitPrice: number;
}

const LINES: Line[] = [
  { description: "Desk lamps, LED", quantity: 24, unitPrice: 48.9 },
  { description: "Printer paper, box of 5 reams", quantity: 40, unitPrice: 21.5 },
  { description: "Delivery", quantity: 1, unitPrice: 35 },
];

export default function ComputedValue() {
  const { Table, Column } = useTable(LINES, { rowKey: (l) => l.description });

  return (
    <Table ariaLabel="Invoice lines">
      <Column value="description" label="Description" rowHeader />
      <Column value="quantity" label="Quantity" />
      <Column value="unitPrice" label="Unit price (€)" format={{ decimals: 2 }} />
      <Column id="net" label="Net (€)" value={(l) => l.quantity * l.unitPrice} format={{ decimals: 2 }} />
    </Table>
  );
}
```

### Let the value type decide

Without `children` the value decides: text left, numbers right with tabular figures, dates as date and time, booleans as a word.

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

interface Service {
  name: string;
  requests: number;
  deployed: Date;
  pages: boolean;
  openIncidents: number | null;
}

const SERVICES: Service[] = [
  { name: "Checkout", requests: 1840.5, deployed: new Date(2026, 2, 17, 9, 5), pages: true, openIncidents: 1 },
  { name: "Reporting", requests: 18, deployed: new Date(2026, 2, 16, 14, 30), pages: false, openIncidents: null },
];

export default function Defaults() {
  const { Table, Column } = useTable(SERVICES, { rowKey: (s) => s.name });

  return (
    <Table ariaLabel="Defaults per value type">
      <Column value="name" label="Text" rowHeader />
      <Column value="requests" label="Number" />
      <Column value="deployed" label="Point in time" />
      <Column value="pages" label="Truth value" />
      <Column value="openIncidents" label="Absent" />
    </Table>
  );
}
```

### Name the row

`rowHeader` makes a column the row's name: it cannot be hidden, and the row's checkbox and actions are read out with it.

```tsx
import { useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { ColumnMenu, Toolbar, useTable } from "@umriss-ui/table";

interface Incident {
  id: string;
  title: string;
}

const INCIDENTS: Incident[] = [
  { id: "INC-1048", title: "Checkout slow, card payments time out" },
  { id: "INC-1047", title: "Webhook deliveries delayed" },
  { id: "INC-1046", title: "Thumbnails missing for new uploads" },
];

export default function RowHeader() {
  const [last, setLast] = useState("Nothing opened yet.");
  const { Table, Column, RowActions, Action } = useTable(INCIDENTS, { rowKey: (i) => i.id });

  return (
    <Stack gap={3}>
      <Table selectable ariaLabel="Incidents">
        <Toolbar>
          <ColumnMenu />
        </Toolbar>
        <Column value="id" label="Incident" rowHeader />
        <Column value="title" label="Title" />
        <RowActions>
          <Action onSelect={(i) => setLast(`${i.id} opened`)}>Open</Action>
        </RowActions>
      </Table>
      <Text size="sm" tone="secondary" role="status">
        {last}
      </Text>
    </Stack>
  );
}
```

## When to use something else

- Restricting a column to some of its values → Filter
- A group key that should not be a column → Grouping
- A measured value read against a limit set → VerdictColumn

## Keyboard

The keys of [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#keyboard-first-table), [ColumnMenu](https://romanhaendler.github.io/umriss-ui/table/columnmenu/#keyboard-columnmenu), [RowActions](https://romanhaendler.github.io/umriss-ui/table/rowactions/#keyboard-rowactions) and [Checkbox](https://romanhaendler.github.io/umriss-ui/core/checkbox/#keyboard-checkbox) apply here.

## Accessibility

The `label` is the column header's text, and so what a screen reader says for every cell under it. The `rowHeader` column's cells are row headers: a reader names the row by them, and the row's checkbox and actions are named after them - “Select INC-1048”, “Open: INC-1048”. Without one, each of those says only its own word.

## API

### `FieldColumn<Z, K extends Field<Z>>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `K` | — | A field of the row. Supplies the column's `id` at the same time.<br>Shown in: [Read a value from a field](https://romanhaendler.github.io/umriss-ui/table/column/#value-from-field), [Compute a value from the row](https://romanhaendler.github.io/umriss-ui/table/column/#computed-value), [Let the value type decide](https://romanhaendler.github.io/umriss-ui/table/column/#defaults) and 118 more |
| `id` | `string` | — | An id of one's own instead of the field name – for two columns over the same field.<br>Shown in: [Pick a standard format (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#format), [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets) |
| `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.<br>Shown in: [Compute a value from the row](https://romanhaendler.github.io/umriss-ui/table/column/#computed-value), [Set wording and formats once (Provider)](https://romanhaendler.github.io/umriss-ui/table/provider/#provider), [Pick a standard format (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#format) and 33 more |
| `filter` | [`FilterFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-FilterFor)`<Z[K]>` | — | A filter in the column header: `"list"` offers the values that occur, `"range"` two bounds (numbers or points in time), a filter from `columnFilter` asks whatever it asks itself. Its condition stands in the table toolbar.<br>Shown in: [A label wider than its column (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#a-label-wider-than-its-column), [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Choose from the values that occur (Filter)](https://romanhaendler.github.io/umriss-ui/table/filter/#list-filter) and 11 more |
| `group` | [`GroupFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-GroupFor)`<Z[K]>` | — | Groups points in time by `"day"`, `"week"`, `"month"` or `"year"`.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Group dates by month (Grouping)](https://romanhaendler.github.io/umriss-ui/table/grouping/#by-month) |
| `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`.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Combine grid mode with groups, pinning and many rows (Grid mode)](https://romanhaendler.github.io/umriss-ui/table/grid-mode/#the-whole-grid), [Edit a comments column (Edits)](https://romanhaendler.github.io/umriss-ui/table/edits/#comments-column) and 6 more |
| `editOptions` | `readonly `[`Present`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Present)`<Z[K]>[]` | the values that occur | What `edit="select"` offers. From `EditOptions`.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Check an edit before it is reported (Edits)](https://romanhaendler.github.io/umriss-ui/table/edits/#validated-edits), [Save a row on purpose (Edits)](https://romanhaendler.github.io/umriss-ui/table/edits/#save-a-row-on-purpose) and 1 more |
| `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`.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Combine grid mode with groups, pinning and many rows (Grid mode)](https://romanhaendler.github.io/umriss-ui/table/grid-mode/#the-whole-grid), [Check an edit before it is reported (Edits)](https://romanhaendler.github.io/umriss-ui/table/edits/#validated-edits) and 3 more |
| `children` | [`Presentation`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Presentation)`<Z[K], Z>` | — | How the value appears: `(value, row)`. Never called for an absent value. Required where the value has no text form.<br>Shown in: [Draw a value with other components (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#presentation), [Show absent values (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#absent-values), [Fit what does not fit (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#values-that-do-not-fit) and 9 more |

Also every prop of `ColumnBase`, `ValuePaths` and `AggregateOptions`.

### `ColumnBase`

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` *required* | `string` | — | Names the column in its header, in the column menu and in the header row of the export.<br>Shown in: [Read a value from a field](https://romanhaendler.github.io/umriss-ui/table/column/#value-from-field), [Compute a value from the row](https://romanhaendler.github.io/umriss-ui/table/column/#computed-value), [Let the value type decide](https://romanhaendler.github.io/umriss-ui/table/column/#defaults) and 118 more |
| `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.<br>Shown in: [Read a value from a field](https://romanhaendler.github.io/umriss-ui/table/column/#value-from-field), [Compute a value from the row](https://romanhaendler.github.io/umriss-ui/table/column/#computed-value), [Let the value type decide](https://romanhaendler.github.io/umriss-ui/table/column/#defaults) and 117 more |
| `numeric` | `boolean` | decided by the first value present | Right-aligned with tabular figures.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets) |
| `width` | `number` | the browser's | Initial width in pixels.<br>Shown in: [Fit long values (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#long-values), [Fit what does not fit (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#values-that-do-not-fit), [Let people set the width (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#width) and 15 more |
| `resizable` | `boolean` | — | Shows the drag grip on the header cell.<br>Shown in: [Let people set the width (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#width), [Pin and size from controls of your own (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pin-and-size-from-controls-of-your-own), [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets) and 1 more |
| `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.<br>Shown in: [Pin columns to both sides (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pinned-both-sides), [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Combine grid mode with groups, pinning and many rows (Grid mode)](https://romanhaendler.github.io/umriss-ui/table/grid-mode/#the-whole-grid) and 1 more |
| `sortable` | `boolean` | `true` when the value is text, a number, a point in time or a boolean | Whether the column can be sorted by.<br>Shown in: [Fit long values (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#long-values), [Fit what does not fit (Formats)](https://romanhaendler.github.io/umriss-ui/table/formats/#values-that-do-not-fit), [Let people set the width (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#width) and 6 more |
| `searchable` | `boolean` | `true` for text, `false` otherwise | Takes part in the search.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Leave a column out (Search)](https://romanhaendler.github.io/umriss-ui/table/search/#leave-a-column-out), [An uneven organisation (Tree rows)](https://romanhaendler.github.io/umriss-ui/table/tree-rows/#an-uneven-organisation) |
| `groupable` | `boolean` | `true` when its value is text, a number, a point in time or a boolean, or it has a `groupValue` | Can the table be grouped by this column?<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Switch grouping off (Grouping)](https://romanhaendler.github.io/umriss-ui/table/grouping/#switched-off) |

### `ValuePaths<W>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `sortValue` | `(value: `[`Present`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Present)`<W>) => string \| number \| `[`Absent`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Absent) | — | What is sorted by, when not by the value itself.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Sort and export a value that is an object (Sorting)](https://romanhaendler.github.io/umriss-ui/table/sorting/#own-ordering), [Work through the alerts (Scenarios)](https://romanhaendler.github.io/umriss-ui/table/#work-through-the-alerts) |
| `exportValue` | `(value: `[`Present`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Present)`<W>) => string \| number \| `[`Absent`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Absent) | — | What stands in the export, when not the value itself.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Sort and export a value that is an object (Sorting)](https://romanhaendler.github.io/umriss-ui/table/sorting/#own-ordering) |
| `groupValue` | `(value: `[`Present`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Present)`<W>) => string \| number \| boolean \| Date \| `[`Absent`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Absent) | — | What is grouped by, when not the value itself - a group header of a number, say. Rows whose group value is absent form a group of their own.<br>Shown in: [Share a column between tables (Presets)](https://romanhaendler.github.io/umriss-ui/table/presets/#presets), [Group numbers in bands (Grouping)](https://romanhaendler.github.io/umriss-ui/table/grouping/#in-bands) |

### Types on this page

#### `FormatFor<W>`

What a column accepts as `format`: a number format for numbers, a date
format for points in time, nothing for every other value.

```ts
type FormatFor<W> = [Present<W>] extends [number]
  ? NumberFormat
  : [Present<W>] extends [Date]
    ? DateFormat
    : never;
```

#### `FilterFor<W>`

What a column accepts as `filter`: `"list"` for every value with a text
form, `"range"` for numbers and points in time, a filter from `columnFilter`
only where its value type fits.

```ts
type FilterFor<W> =
  | (IsDisplayable<W> extends true ? "list" : never)
  | ([Present<W>] extends [number] ? "range" : [Present<W>] extends [Date] ? "range" : never)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the condition belongs to the filter; at the column only the value counts
  | ColumnFilter<Present<W>, any>;
```

#### `GroupFor<W>`

`group` on a point in time: its rows are grouped by day, ISO week, month or
year.

```ts
type GroupFor<W> = [Present<W>] extends [Date] ? DatePeriod : never;
```

#### `EditFor<W, Z>`

What a column accepts as `edit`: the core field for its value - `"text"`
for text, `"number"` for numbers, `"date"` for points in time, `"select"`
for text and numbers - or an editor of one's own.

```ts
type EditFor<W, Z> =
  | ([Present<W>] extends [string] ? "text" | "select" : never)
  | ([Present<W>] extends [number] ? "number" | "select" : never)
  | ([Present<W>] extends [Date] ? "date" : never)
  | ((editor: CellEditorProps<W, Z>) => ReactNode);
```

#### `Present<W>`

The value without the absence - that is what `children` gets.

```ts
type Present<W> = Exclude<W, Absent>;
```

#### `Presentation<W, Z>`

What a column draws in a cell: the value present and its row.

```ts
type Presentation<W, Z> = (value: Present<W>, row: Z) => ReactNode;
```

#### `Pin`

Where a pinned column stands: in the block before or after the others.

```ts
type Pin = "start" | "end";
```

#### `Absent`

A value that is not there: `null` or `undefined`.

```ts
type Absent = null | undefined;
```

#### `NumberFormat`

The names of the standard presentations of a number.

```ts
type NumberFormat = "percent" | "count" | { decimals: number };
```

#### `DateFormat`

The names of the standard presentations of a point in time.

```ts
type DateFormat = "date" | "time" | "dateTime";
```

#### `IsDisplayable<W>`

`true` where the value present can appear as text without `children`
(`Displayable`), `false` otherwise - what decides the filters and
aggregates a column offers.

```ts
type IsDisplayable<W> = [Present<W>] extends [never]
  ? false
  : [Present<W>] extends [Displayable]
    ? true
    : false;
```

#### `DatePeriod`

The span a point in time is grouped by.

```ts
type DatePeriod = "day" | "week" | "month" | "year";
```

#### `CellEditorProps<W, Z>`

What an editor of one's own receives. The table holds the draft; Enter,
Escape and Tab stay the table's, bubbling up from the field.

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `W \| null` | — | The draft - the cell's value until the field changes it. |
| `commit` *required* | `(value?: W \| null) => void` | — | Commits - the draft, or the value given. A picker commits on its pick. |
| `row` *required* | `Z` | — |  |
| `invalid` *required* | `boolean` | — | The draft did not validate: the field shows it; the message stands beneath. |
| `label` *required* | `string` | — | The field's name: what is edited, for which row. The `FormField` around the editor already names a core field by it. |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onChange` *required* | `(value: W \| null) => void` | — | Changes the draft. |

#### `Displayable`

What can appear as text without `children`.

```ts
type Displayable = string | number | boolean | Date;
```
