# Width and pinning

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

How wide a column stands, which columns stay in view while the table scrolls sideways (frozen or sticky columns), and a header that stays while it scrolls down.

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

Demo page: https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/

## Examples

### Let people set the width

`width` is the starting width, `resizable` adds a grip: drag it, double-click it to fit the content, or press Alt and an arrow.

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

interface Shipment {
  id: string;
  customer: string;
  note: string;
}

const SHIPMENTS: Shipment[] = [
  { id: "FP-1004210", customer: "Holloway Garden Supplies", note: "Goods entrance at the back, ring twice" },
  { id: "FP-1004223", customer: "Oakridge Pharmacy", note: "Cold chain, hand over in person" },
  { id: "FP-1004236", customer: "Brixley Cycles", note: "Leave with the neighbour at number 90 if the shop is closed" },
];

export default function Width() {
  const { Table, Column } = useTable(SHIPMENTS, { rowKey: (s) => s.id });

  return (
    <Table ariaLabel="Shipments with a note">
      <Column value="id" label="Shipment" rowHeader width={120} />
      <Column value="customer" label="Customer" resizable width={180} />
      <Column value="note" label="Note" resizable sortable={false} width={160} />
    </Table>
  );
}
```

### Pin columns to both sides

`pin="start"` or `"end"` keeps a column at that edge while the rest scrolls; people pin and unpin in the column menu too.

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

/* Data from the plant world, written out here so the example runs on its own. */
const STATIONS = [
  { id: "saw", label: "Saw 1" },
  { id: "lathe-1", label: "Lathe 1" },
  { id: "lathe-2", label: "Lathe 2" },
  { id: "mill", label: "Mill" },
  { id: "press", label: "Press 2" },
  { id: "paint", label: "Paint shop" },
  { id: "qa", label: "Inspection" },
] as const;

/* The minimum width comes from the application's stylesheet: a table over
   the full width shares out the space it has, and nothing would scroll. */
const STYLE = `
.hourly-output table {
  min-width: 1500px;
}
`;

const HOURS = ["06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17"];

interface Machine {
  name: string;
  area: string;
  output: number[];
  temperature: number | null;
}

const TEMPERATURE: LimitSet = {
  target: 60,
  limits: [
    { value: 72, side: "upper", severity: "warning" },
    { value: 80, side: "upper", severity: "alarm" },
  ],
};

const AREAS = ["Cutting", "Turning", "Forming", "Finishing"];

const ROWS: Machine[] = STATIONS.map(({ label: name }, i) => ({
  name,
  area: AREAS[Math.floor(i / 2)]!,
  output: HOURS.map((_, h) => 30 + ((i * 11 + h * 7) % 29)),
  temperature: i === 5 ? null : 52 + ((i * 9) % 31),
}));

export default function PinnedBothSides() {
  const { Table, Column, VerdictColumn, RowActions, Action } = useTable(ROWS, { rowKey: (m) => m.name });
  const [last, setLast] = useState("Nothing opened yet");

  return (
    <Stack gap={2}>
      <style>{STYLE}</style>
      <Table className="hourly-output" selectable ariaLabel="Output per hour">
        <Toolbar>
          <ColumnMenu />
        </Toolbar>
        <Column value="area" label="Area" width={110} />
        {HOURS.map((hour, h) => (
          <Column key={hour} id={`h${hour}`} label={`${hour}:00`} value={(m) => m.output[h]} aggregate="sum" width={80} />
        ))}
        <Column value="name" label="Machine" rowHeader pin="start" width={130} />
        <VerdictColumn value="temperature" label="Temperature (°C)" limits={TEMPERATURE} pin="end" aggregate="worst" width={150} />
        <RowActions>
          <Action onSelect={(m) => setLast(`${m.name} opened`)}>Open</Action>
        </RowActions>
      </Table>
      <Text size="xs" tone="muted">
        {last}
      </Text>
    </Stack>
  );
}
```

### Keep the header and the names in view

`stickyHeader` holds the header row while the rows scroll, `stickyRowHeader` the name column while the days scroll sideways.

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

/* `width` is an initial width: a table over the full width shares out the
   space it has, so the minimum width that makes it scroll comes from the
   application's stylesheet. */
const STYLE = `
.week-table table {
  min-width: 1400px;
}
`;

const DAYS = ["Mon 16", "Tue 17", "Wed 18", "Thu 19", "Fri 20", "Mon 23", "Tue 24", "Wed 25", "Thu 26", "Fri 27"];

interface Person {
  id: string;
  name: string;
  team: string;
  role: string;
  hours: number[];
}

const PEOPLE: Person[] = [
  ["Maya Lindgren", "Web", "Product manager"],
  ["Arjun Mehta", "Web", "Developer"],
  ["Chloe Durand", "Web", "Developer"],
  ["Noah Fischer", "Web", "Designer"],
  ["Eva Novak", "Web", "QA engineer"],
  ["Luis Moreno", "Apps", "Product manager"],
  ["Hana Sato", "Apps", "Developer"],
  ["Kofi Mensah", "Apps", "Developer"],
  ["Freya Olsen", "Apps", "Designer"],
  ["David Kowalski", "Apps", "QA engineer"],
].map(([name, team, role], i) => ({
  id: `p${i + 1}`,
  name: name!,
  team: team!,
  role: role!,
  hours: DAYS.map((_, d) => 4 + ((i * 5 + d * 3) % 5)),
}));

export default function StickyParts() {
  const { Table, Column, RowDetail } = useTable(PEOPLE, { rowKey: (p) => p.id });

  return (
    <>
      <style>{STYLE}</style>
      <Table className="week-table" selectable stickyHeader stickyRowHeader maxHeight="300px" ariaLabel="Planned hours per day">
        <Column value="team" label="Team" width={96} />
        <Column value="name" label="Person" rowHeader width={160} />
        {DAYS.map((day, d) => (
          <Column key={day} id={day} label={day} value={(p) => p.hours[d]} width={96} />
        ))}
        <RowDetail>
          {(p) => (
            <Text size="sm">
              {p.name}, {p.role}, team {p.team}
            </Text>
          )}
        </RowDetail>
      </Table>
    </>
  );
}
```

### Pin and size from controls of your own

`t.setPin` pins a column or, with `null`, unpins it; `t.setWidth` sets a width and `undefined` takes it back. `t.pinned` and `t.widths` say how the columns stand, a dragged grip included.

```tsx
import { Button, Stack, Switch, Text } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";

/* The minimum width comes from the application's stylesheet, so that the
   table scrolls sideways and a pinned column has something to hold against. */
const STYLE = `
.delivery-notes table {
  min-width: 1100px;
}
`;

interface Shipment {
  id: string;
  customer: string;
  town: string;
  window: string;
  weight: number;
  note: string;
}

const SHIPMENTS: Shipment[] = [
  { id: "FP-1004210", customer: "Holloway Garden Supplies", town: "Ashcombe", window: "08:00–10:00", weight: 23, note: "Goods entrance at the back, ring twice" },
  { id: "FP-1004223", customer: "Oakridge Pharmacy", town: "Oakridge", window: "08:00–10:00", weight: 4, note: "Cold chain, hand over in person" },
  { id: "FP-1004236", customer: "Brixley Cycles", town: "Brixley", window: "10:00–12:00", weight: 14, note: "Leave with the neighbour at number 90 if the shop is closed" },
  { id: "FP-1004249", customer: "Tamsin's Bakery", town: "Pellham", window: "06:00–08:00", weight: 31, note: "Before opening, side door" },
];

export default function PinAndSizeFromControls() {
  const t = useTable(SHIPMENTS, { rowKey: (s) => s.id });
  const { Table, Column } = t;
  const wide = t.widths.note === 420;

  return (
    <Stack gap={3}>
      <style>{STYLE}</style>
      <Stack direction="row" gap={4} align="center" wrap>
        <Switch
          label="Keep the customer in view"
          checked={t.pinned.customer === "start"}
          onChange={(event) => t.setPin("customer", event.target.checked ? "start" : null)}
        />
        <Button size="sm" onClick={() => t.setWidth("note", wide ? undefined : 420)}>
          {wide ? "Note back to its width" : "Widen the note"}
        </Button>
        <Text size="sm" tone="secondary">
          Note: {t.widths.note} px
        </Text>
      </Stack>
      <Table className="delivery-notes" ariaLabel="Shipments with a note">
        <Column value="id" label="Shipment" rowHeader width={120} />
        <Column value="customer" label="Customer" width={200} />
        <Column value="town" label="Town" width={120} />
        <Column value="window" label="Window" width={120} />
        <Column value="weight" label="Weight (kg)" width={110} />
        <Column value="note" label="Note" resizable sortable={false} width={180} />
      </Table>
    </Stack>
  );
}
```

### A label wider than its column

A column with a `width` is that wide, whatever its label says. The label ends in an ellipsis; the sort arrow and the filter stay.

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

interface Shipment {
  id: string;
  weight: number;
  carrier: string;
}

const SHIPMENTS: Shipment[] = [
  { id: "FP-1004210", weight: 12.4, carrier: "Northline" },
  { id: "FP-1004223", weight: 3.1, carrier: "Eastway" },
  { id: "FP-1004236", weight: 48, carrier: "Northline" },
];

export default function ALabelWiderThanItsColumn() {
  const { Table, Column } = useTable(SHIPMENTS, { rowKey: (s) => s.id, defaultSort: { column: "weight", direction: "desc" } });

  return (
    <Table ariaLabel="Shipments in narrow columns">
      <Column value="id" label="Shipment" rowHeader />
      <Column value="weight" label="Gross weight in kilograms" width={75} />
      <Column value="carrier" label="Carrier responsible for delivery" width={75} filter="list" />
    </Table>
  );
}
```

## Keyboard

| Key | Action |
|---|---|
| `Alt + → / Alt + ←` | On a resizable header: wider or narrower. Alt + Shift + → and Alt + Shift + ← take larger steps. |
| `Alt + Home` | On a resizable header: fits the width to the content. |

## 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. |
| [`stickyRowHeader`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-stickyRowHeader) | `boolean` | — | Keeps the row header column visible while scrolling sideways - the same as `pin="start"` on the row header. |
| [`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. |
| [`children`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-children) | `ReactNode` | — | Columns, `RowDetail`, `RowActions` and the unbound parts. |

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

#### Styling

| Prop | Type | Default | Description |
|---|---|---|---|
| [`className`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-className) | `string` | — | Classes on the outer frame of the table. |

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

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`pinned`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-pinned) *required* | `Readonly<Record<string, `[`Pin`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-Pin)`>>` | — | The pinned columns by id - the declared ones until the user chooses. |
| [`setPin`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-setPin) *required* | `(column: string, pin: `[`Pin`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-Pin)` \| null) => void` | — | Pins a column to the start or the end, or unpins it with `null`. |
| [`widths`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-widths) *required* | `Readonly<Record<string, number>>` | — | The widths in pixels: those of the columns, and over them the dragged ones. |
| [`setWidth`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableSnapshot-setWidth) *required* | `(column: string, width: number \| undefined) => void` | — | Sets the width of a column; `undefined` takes it back. |

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. |
| [`filter`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-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. |

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. |
| [`resizable`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-resizable) | `boolean` | — | Shows the drag grip on the header cell. |
| [`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. |
| [`sortable`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-sortable) | `boolean` | `true` when the value is text, a number, a point in time or a boolean | Whether the column can be sorted by. |

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/rowdetail/#RowDetailProps-children) *required* | `(row: Z) => ReactNode` | — | What stands underneath an expanded row. |

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

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/toolbar/#ToolbarProps-children) | `ReactNode` | — | Search, column menu, export – on the left in the table toolbar, before the conditions. |

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

| Prop | Type | Default | Description |
|---|---|---|---|
| [`label`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#VerdictBase-label) *required* | `string` | — | Names the column in its header, in the column menu and in the header row of the export. |
| [`limits`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#VerdictBase-limits) *required* | [`LimitSet`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-LimitSet) | — | The limit set every value is read against. |
| [`width`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#VerdictBase-width) | `number` | the browser's | Initial width in pixels. |
| [`pin`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#VerdictBase-pin) | [`Pin`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-Pin)<br>`"start" \| "end"` | — | Keeps the column in view while the table scrolls sideways, before or after every other column. |
| [`aggregate`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#VerdictBase-aggregate) | `"worst"` | — | `"worst"`: the worst verdict among the rows – in the footer and in a group's header. |
