Table
Presentational parts over a plain <table>. Four separator styles, two densities, a sortable column header, a fixed selection column, and the cell shapes a data table repeats most. Nothing here holds state — sorting, selection and paging stay the caller's, so a headless table layer can be dropped on top later without restyling any of this.
import {
Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell,
TableSortHeader, TableSelectHead, TableSelectCell,
TableCellStack, TableActions, TableEmpty, TableSkeletonRows,
} from "@pestoui/core"Runs table
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Probate & FSBO Lead Hunter | Running | 863 |
| Inbox Triage Assistant | Failed | 97 |
Separators
How rows are told apart. row is the default hairline under every row; zebra alternates a faint fill instead; grid adds vertical rules for dense numeric columns; none removes the divisions entirely and leaves whitespace to do the work. Zebra re-states hover, the expanded hold and the selected fill at its own specificity, so a striped row still lights up under the pointer — a stripe that goes dead on hover is the usual bug here.
separators="row"
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Probate & FSBO Lead Hunter | Running | 863 |
| Inbox Triage Assistant | Failed | 97 |
separators="zebra"
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Probate & FSBO Lead Hunter | Running | 863 |
| Inbox Triage Assistant | Failed | 97 |
separators="grid"
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Probate & FSBO Lead Hunter | Running | 863 |
| Inbox Triage Assistant | Failed | 97 |
separators="none"
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Probate & FSBO Lead Hunter | Running | 863 |
| Inbox Triage Assistant | Failed | 97 |
Density
The size axis moves padding and row height only — never font-size. A table that shrinks its own text stops matching the page around it. Density travels as three custom properties rather than per-part variants, so a cell can never disagree with the row it sits in, and size is deliberately left unset by default: an ancestor that declares those properties — a Table Card, for instance — becomes the single owner of density for everything inside it, and a plain Table simply inherits. Pass size on the table only to override a shell that disagrees.
| Agent | Status | Total runs |
|---|---|---|
| Niche Content Composer | Success | 1,204 |
| Inbox Triage Assistant | Failed | 97 |
Sorting and selection
Both axes are controlled, and neither is implemented here. TableSortHeader renders the arrow, sets aria-sort and calls onSort with the direction you should move to — it does not sort anything. TableSelectHead and TableSelectCell only fix the column to a stable width so the checkbox stops sliding as the widest cell changes between pages; you supply the Checkbox, and select-all goes half-checked by passing checked="indeterminate". A selected row is marked with data-state="selected" and tints with the selected green, the same token the checkbox fills with. The idle sort chevron is held at zero opacity rather than unmounted, so the column keeps its width whether or not the pointer is over it.
| Agent | ||
|---|---|---|
Niche Content Composercontent@example.com | 1,204 | |
Probate & FSBO Lead Huntersales@example.com | 863 | |
Inbox Triage Assistantops@example.com | 97 |
Cell layout
TableCellStack is the one cell shape worth owning — a bold primary over a muted secondary, with an optional leading media slot. Every other pattern inside a cell (a status badge, a progress bar, an avatar group, a rating) is an existing component dropped straight into a TableCell, and wrapping those would only duplicate their props. TableActions holds the trailing icon buttons and is visible at rest on purpose: actions that fade in on row hover are undiscoverable, and on a touch screen unreachable.
| Agent | Status | Actions |
|---|---|---|
Niche Content Composercontent@example.com | Success | |
Inbox Triage Assistantops@example.com | Failed | |
| 2 agents | 1,301 runs | |
Sticky header
A sticky header needs a bounded scroller to stick against, and that scroller is the container every table already renders inside — reach it through containerProps. The header hairline is drawn by a pseudo-element rather than a border here: a collapsed border belongs to the table rather than the cell, so it would stay behind with the body and the header would visibly shed its underline mid-scroll.
| Run | Started | Duration |
|---|---|---|
| Run #1042 | 1h ago | 12s |
| Run #1041 | 2h ago | 15s |
| Run #1040 | 3h ago | 18s |
| Run #1039 | 4h ago | 21s |
| Run #1038 | 5h ago | 24s |
| Run #1037 | 6h ago | 27s |
| Run #1036 | 7h ago | 30s |
| Run #1035 | 8h ago | 33s |
| Run #1034 | 9h ago | 36s |
| Run #1033 | 10h ago | 39s |
Empty and loading
TableEmpty renders the row as well as the cell, because the row is the part that keeps getting written wrong: an empty state that inherits the row hover fill reads as a clickable record. Drop a small EmptyState inside it. TableSkeletonRows takes one entry per column and offsets each row against the stagger token, turning a block pulsing in unison — which reads as one big flashing rectangle — into a sweep travelling down the table. Set aria-busy on the Table while they are up; the placeholder rows are hidden from assistive tech, since reading out thirty-five empty cells is worse than silence.
| Agent | Status | Total runs |
|---|---|---|
| Agent | Status | Total runs |
|---|
Props
Pesto UI · 117 documented components