Skip to content

Table

In the app

The data table as the app draws it: soft grid lines between columns, a fill-soft header, hover rows, and a selected row in the accent wash.

@varick/ui/components/tableAdded 2 Sept 2026

Agents 4

1 selected
SuccessStatus
Submission router34294.2%Running
Renewal manager15688.1%Review
Compliance check6199.0%Queued
Invoice matcher1271.4%Failed

Usage

import {
  Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
} from "@varick/ui/components/table";

export function Example() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead sortable sorted="asc">Agent</TableHead>
          <TableHead className="text-right">Runs</TableHead>
          <TableHead>Status</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((r) => (
          <TableRow key={r.id} selected={selected.has(r.id)}>
            <TableCell strong>{r.name}</TableCell>
            <TableCell align="right">{r.runs}</TableCell>
            <TableCell><Status tone={r.tone}>{r.status}</Status></TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

API reference

Every component also accepts className and the native props of its root element.

PropTypeDefaultDescription
TableRow.selectedbooleanfalseAccent wash and data-state="selected".
TableHead.sortablebooleanfalseRenders the label as a button with a sort glyph.
TableHead.sorted'asc' | 'desc'undefinedSets the glyph and aria-sort.
TableCell.strongbooleanfalseInk and medium weight, for the row’s name.
TableCell.align'left' | 'right''left'Right also sets tabular numerals.

Accessibility

  • A real <table>: header cells are <th>, sorted columns carry aria-sort.
  • The wrapper scrolls horizontally so narrow screens never squash the columns.
  • Selection checkboxes need aria-label with the row name, and a "Select all" in the header.

Styles

The CSS as it appears in the app. The live component above is the same values as Tailwind classes.

table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
  text-align: left;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fg-mid);
  padding: 9px 16px;
  border-bottom: 1px solid var(--hairline-soft);
  border-right: 1px solid var(--hairline-soft);
  white-space: nowrap;
  background: var(--fill-soft);
}
tbody td {
  padding: 10px 16px;
  border-bottom: 1px solid var(--hairline-soft);
  border-right: 1px solid var(--hairline-soft);
  color: var(--fg-mid);
}
thead th:last-child, tbody td:last-child { border-right: none; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--fill-soft); }
tbody tr:has(.tbl-check:checked) { background: var(--accent-fade); }
td.strong { color: var(--fg-high); font-weight: 500; }
.th-sort { display: inline-flex; align-items: center; gap: 3px; }