Row appearance

How rows look: dense or regular, marked by their state, and what stands while rows load, fail or do not exist.

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

A row's mark is the application's stylesheet at work; say the same in a word in a column too, so the colour is never the only sign.

Examples

Density

density="compact" sets one table; an application that wants it dense everywhere says so once at the UmrissProvider.

import { UmrissProvider, Stack, Text } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";

interface Service {
  name: string;
  team: string;
  p95: number;
}

const SERVICES: Service[] = [
  { name: "Checkout", team: "Payments", p95: 412 },
  { name: "Billing", team: "Payments", p95: 238 },
  { name: "Sign-in", team: "Identity", p95: 141 },
];

function Services({ caption, density }: { caption: string; density?: "regular" | "compact" }) {
  const { Table, Column } = useTable(SERVICES, { rowKey: (s) => s.name });
  return (
    <Stack gap={2}>
      <Text size="xs" tone="muted">
        {caption}
      </Text>
      <Table density={density} ariaLabel={`Services, ${caption}`}>
        <Column value="name" label="Service" rowHeader />
        <Column value="team" label="Team" />
        <Column value="p95" label="p95 latency (ms)" />
      </Table>
    </Stack>
  );
}

export default function Density() {
  return (
    <Stack gap={5}>
      <Services caption="Without a statement: regular" />
      <Services caption='density="compact"' density="compact" />
      <UmrissProvider density="compact">
        <Services caption="From the UmrissProvider: compact" />
      </UmrissProvider>
    </Stack>
  );
}

Mark rows by their state

rowProps gives a row classes and data attributes; the application's stylesheet draws them, and a word in a column says the same.

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

interface Invoice {
  id: string;
  supplier: string;
  due: Date;
  overdue: boolean;
}

const INVOICES: Invoice[] = [
  { id: "INV-26-0318", supplier: "Brandlow Office Supply", due: new Date(2026, 3, 15), overdue: false },
  { id: "INV-26-0309", supplier: "Nimbrel Software", due: new Date(2026, 3, 8), overdue: false },
  { id: "INV-26-0226", supplier: "Corrin Travel", due: new Date(2026, 2, 12), overdue: true },
  { id: "INV-26-0221", supplier: "Stellbrook Consulting", due: new Date(2026, 2, 16), overdue: true },
];

const STYLE = `
.invoice-list tr[data-overdue] > th {
  box-shadow: inset 3px 0 0 var(--u-color-warning);
}
`;

export default function StylingRows() {
  const { Table, Column } = useTable(INVOICES, { rowKey: (i) => i.id });

  return (
    <>
      <style>{STYLE}</style>
      <Table className="invoice-list" ariaLabel="Invoices" rowProps={(i) => ({ "data-overdue": i.overdue ? "" : undefined })}>
        <Column value="id" label="Invoice" rowHeader />
        <Column value="supplier" label="Supplier" />
        <Column value="due" label="Due" format="date" />
        <Column value="overdue" label="Overdue" />
      </Table>
    </>
  );
}

Show loading, empty, failed and no match

loading shows placeholder rows where there are none yet, and dims the rows that stand while new ones are on their way; empty what stands without rows, a failed load too; that nothing matches the table recognises itself.

import type { ReactNode } from "react";
import { Alert, Button, EmptyState, Stack } from "@umriss-ui/core";
import { Search, Toolbar, useTable } from "@umriss-ui/table";

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

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

const NONE: Incident[] = [];

function Incidents({ rows, label, ...props }: { rows: Incident[]; label: string; loading?: boolean; empty?: ReactNode; search?: string }) {
  const { Table, Column } = useTable(rows, { rowKey: (i) => i.id, initialView: props.search ? { search: props.search } : undefined });
  return (
    <Table ariaLabel={label} loading={props.loading} empty={props.empty}>
      {props.search !== undefined && (
        <Toolbar>
          <Search placeholder="Search incidents" />
        </Toolbar>
      )}
      <Column value="id" label="Incident" rowHeader />
      <Column value="title" label="Title" />
      <Column value="severity" label="Severity" />
    </Table>
  );
}

export default function States() {
  return (
    <Stack gap={5}>
      <Incidents rows={NONE} label="Incidents are loading" loading />
      <Incidents
        rows={NONE}
        label="Open incidents"
        empty={<EmptyState title="No open incidents" description="New incidents appear here as soon as an alert opens one." />}
      />
      <Incidents
        rows={NONE}
        label="Incidents, not loaded"
        empty={
          <Alert tone="danger" title="The incidents could not be loaded" actions={<Button size="sm">Try again</Button>}>
            The status service did not answer within 10 seconds.
          </Alert>
        }
      />
      <Incidents rows={INCIDENTS} label="Incidents, searched" search="database" />
    </Stack>
  );
}

Stripes across a wide table

striped shades every other row, so the eye keeps a long row on its line across eight columns, and on a phone while it scrolls sideways.

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

interface Shipment {
  id: string;
  customer: string;
  street: string;
  city: string;
  tour: string;
  driver: string;
  parcels: number;
  weight: number;
}

const SHIPMENTS: Shipment[] = [
  { id: "FP-1004210", customer: "Holloway Garden Supplies", street: "14 Orchard Lane", city: "Ashford", tour: "T-01", driver: "Tomasz Nowak", parcels: 6, weight: 1140 },
  { id: "FP-1004223", customer: "Oakridge Pharmacy", street: "2 Station Road", city: "Wye", tour: "T-01", driver: "Tomasz Nowak", parcels: 2, weight: 420 },
  { id: "FP-1004236", customer: "Brixley Cycles", street: "88 High Street", city: "Charing", tour: "T-02", driver: "Leila Haddad", parcels: 4, weight: 310 },
  { id: "FP-1004249", customer: "Pellham Hardware", street: "5 Forge Yard", city: "Lenham", tour: "T-02", driver: "Leila Haddad", parcels: 9, weight: 1620 },
  { id: "FP-1004262", customer: "Corrin Travel", street: "31 Bank Street", city: "Ashford", tour: "T-03", driver: "Jonas Keller", parcels: 1, weight: 12 },
  { id: "FP-1004275", customer: "Nimbrel Software", street: "7 Kennington Road", city: "Willesborough", tour: "T-03", driver: "Jonas Keller", parcels: 3, weight: 95 },
];

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

  return (
    <Table striped stickyRowHeader ariaLabel="Shipments with their addresses">
      <Column value="id" label="Shipment" rowHeader />
      <Column value="customer" label="Customer" />
      <Column value="street" label="Street" />
      <Column value="city" label="Town" />
      <Column value="tour" label="Tour" />
      <Column value="driver" label="Driver" />
      <Column value="parcels" label="Parcels" />
      <Column value="weight" label="Weight (kg)" />
    </Table>
  );
}

Keyboard

The keys of First table, Input and Button apply here.

Accessibility

While loading is set the table is aria-busy: a screen reader waits for the rows instead of reading the placeholders. Rows that stand while new ones load stay readable.

What stands in empty is the application's, and it is read where it stands. A failed load that must be heard on arrival goes there as core's Alert with tone="danger", which announces itself. That nothing matches a search or a filter the table says itself.

Props on this page

From TableProps — the full table stands on First table.

NameTypeDefaultDescription
stickyRowHeaderboolean—Keeps the row header column visible while scrolling sideways - the same as pin="start" on the row header.
stripedboolean—Discreet zebra stripes.
density"regular" | "compact"the density of the UmrissProvider, else "regular"How tightly the rows stand.
emptyReactNode—What stands in the body when there are no rows. Empty because of a filter the table shows itself – with the way back.
loadingboolean—Shows placeholder rows instead of the rows.
rowProps(row: Z) => RowAttributes—Classes and data attributes per row.
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.
initialViewTableView—The view to start from, read once when the hook first runs; a different value on a later render is ignored, as React ignores a changed defaultValue. To go to a view later, call setView. The table remembers none: where a view is kept is the application's decision. Names no column carries fall out.

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.

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.

From SearchProps — the full table stands on Search.

NameTypeDefaultDescription
placeholderstringthe wording's "Search …"The placeholder in the field.

From ToolbarProps — the full table stands on Toolbar.

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

Every page of @umriss-ui/table

Getting started

Columns

Finding rows

Grouping

Rows

Around the table

Many rows

Editing

Limits and alarms

API index