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";
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="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>
);
}
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>
</>
);
}
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 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>
);
}
| 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. |
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. |
stickyRowHeader | boolean | — | Keeps the row header column visible while scrolling sideways - the same as
pin="start" on the row header. |
maxHeight | string | — | Height of the scroll area, e.g. "480px". A virtualised table needs it. |
children | ReactNode | — | Columns, RowDetail, RowActions and the unbound parts. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name of the table (not of the outer frame). |
| Name | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes on the outer frame of the table. |
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. |
From TableSnapshot — the full table stands on First table.
| Name | Type | Default | Description |
|---|---|---|---|
pinnedrequired | Readonly<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. |
widthsrequired | Readonly<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.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | K | — | A field of the row. Supplies the column's id at the same time. |
filter | 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.
| 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. |
resizable | boolean | — | Shows the drag grip on the header cell. |
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. |
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.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | (row: Z) => ReactNode | — | What stands underneath an expanded row. |
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. |
From ToolbarProps — the full table stands on Toolbar.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Search, column menu, export – on the left in the table toolbar, before the conditions. |
From VerdictBase — the full table stands on VerdictColumn.
| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the column in its header, in the column menu and in the header row of the export. |
limitsrequired | LimitSet | — | The limit set every value is read against. |
width | number | the browser's | Initial width in pixels. |
pin | Pin"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. |