Width and pinning

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.

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

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.

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.

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.

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.

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.

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.

NameTypeDefaultDescription
selectableboolean—Adds the selection column. "All" means: the filtered set.
stickyHeaderboolean—Keeps the header row visible while scrolling.
stickyRowHeaderboolean—Keeps the row header column visible while scrolling sideways - the same as pin="start" on the row header.
maxHeightstring—Height of the scroll area, e.g. "480px". A virtualised table needs it.
childrenReactNode—Columns, RowDetail, RowActions and the unbound parts.

Accessibility

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

Styling

NameTypeDefaultDescription
classNamestring—Classes on the outer frame of the table.

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.

From TableSnapshot — the full table stands on First table.

NameTypeDefaultDescription
pinnedrequiredReadonly<Record<string, Pin>>—The pinned columns by id - the declared ones until the user chooses.
setPinrequired(column: string, pin: Pin | null) => void—Pins a column to the start or the end, or unpins it with null.
widthsrequiredReadonly<Record<string, number>>—The widths in pixels: those of the columns, and over them the dragged ones.
setWidthrequired(column: string, width: number | undefined) => void—Sets the width of a column; undefined takes it back.

From FieldColumn — the full table stands on Column.

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

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.
resizableboolean—Shows the drag grip on the header cell.
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.
sortablebooleantrue when the value is text, a number, a point in time or a booleanWhether the column can be sorted by.

From RowDetailProps — the full table stands on RowDetail.

NameTypeDefaultDescription
childrenrequired(row: Z) => ReactNode—What stands underneath an expanded row.

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.

From ToolbarProps — the full table stands on Toolbar.

NameTypeDefaultDescription
childrenReactNode—Search, column menu, export – on the left in the table toolbar, before the conditions.

From VerdictBase — the full table stands on VerdictColumn.

NameTypeDefaultDescription
labelrequiredstring—Names the column in its header, in the column menu and in the header row of the export.
limitsrequiredLimitSet—The limit set every value is read against.
widthnumberthe browser'sInitial width in pixels.
pinPin
"start" | "end"
—Keeps the column in view while the table scrolls sideways, before or after every other column.
aggregate"worst"—"worst": the worst verdict among the rows – in the footer and in a group's header.

Every page of @umriss-ui/table

Getting started

Columns

Finding rows

Grouping

Rows

Around the table

Many rows

Editing

Limits and alarms

API index