Table Card

The shell a data table needs before it stops looking like a prototype: a header block carrying the title, a count and the actions, a body the rows sit flush inside, a footer for the result count and pagination, and a bulk-action bar that rises over the rows it acts on. Like the table primitives underneath it, nothing here holds state — sorting, selection, paging and the loading flag are all yours. No headless table library is installed, and none is needed: the demos below wire every one of those with plain React state.

tsx
import {
  TableCard, TableCardHeader, TableCardTitle, TableCardDescription,
  TableCardCount, TableCardActions, TableCardBody, TableCardFooter,
  TableBulkBar, TableBulkBarAction,
} from "@pestoui/core"

The three blocks. TableCardHeader is a grid, so the title, the description and the action cluster place themselves — no wrapper around the left column, and a header with no description collapses to one row on its own. TableCardBody carries no padding at all: the table draws its own edge inset, which is what lets rows run flush to the card's rounded corners. The footer takes the result count on the left and Pagination on the right, and Pagination centres itself by default, so it needs mx-0 w-auto justify-end here.

Agents24
Everything running in this workspace.
AgentOwnerRuns
Guest Conciergeguests@example.com1284
Content Composercontent@example.com642
Ticket Triagesupport@example.com96
Showing 1–3 of 24

Cell content

The four shapes a row repeats. TableCellStack is the name-over-email pair with an optional media slot on the inline-start side; the rest are existing components dropped straight into a TableCell — a StatusBadge, a Progress bar beside its own number, and TableActions holding the trailing icon buttons. Those actions stay visible rather than fading in on row hover: actions that appear under the pointer are undiscoverable, and on a touch screen unreachable.

Agents
AgentStatusAccuracyActions
Guest Conciergeguests@example.com
Running
92%
Content Composercontent@example.com
Paused
74%
Ticket Triagesupport@example.com
Stopped
41%

Compact density

size on the card is the single owner of density: it sets the card's own spacing and the table's cell padding together, so the header, the rows and the footer can never end up on different scales. Only padding and row height move — the text stays put, because a table that shrinks its own type stops matching the page around it. A <Table size> inside still wins if one section really needs to disagree.

Agents3
AgentOwnerRuns
Guest Conciergeguests@example.com1284
Content Composercontent@example.com642
Ticket Triagesupport@example.com96

Sortable headers

TableSortHeader is controlled and does not sort anything. It renders the arrow for whatever direction you hand it and calls onSort with the direction to move to next; deciding the order and reordering the rows is the caller's job — here, one useState and an Array.sort. Pass null (or leave it off) on every column that is not the sorted one, and align="end" on numeric columns so the arrow sits inside the right-aligned text.

Agents
Owner
Guest Conciergeguests@example.com1284
Content Composercontent@example.com642
Ticket Triagesupport@example.com96

Selection and bulk actions

Same deal: the checkboxes report, they do not select. Keep the chosen ids in state, mark each chosen row with data-state="selected" for the row tint, and drive the select-all box from the same list — checked="indeterminate" when some but not all are picked. TableBulkBar reads open and count off that state and rises over the rows on the panel spring. It is anchored inside TableCardBody rather than portalled, which puts it immediately after the rows in the tab order instead of at the end of the document.

Agents3
AgentOwner
Guest Conciergeguests@example.com
Content Composercontent@example.com
Ticket Triagesupport@example.com

Loading

TableSkeletonRows fills the body while the request is in flight. Give it one column entry per header cell and keep each width close to the real content — a placeholder of the wrong width makes the table jump when the data lands, which is worse than no placeholder at all. The rows are hidden from assistive tech and each one is offset against the stagger token, so the block sweeps down the table instead of flashing as one rectangle. Set aria-busy on the table while they are up.

Agents
AgentOwnerRuns

Empty

TableEmpty renders the row as well as the cell, because the row is the part that goes wrong: an empty state inheriting the row hover fill looks like a clickable record. colSpan has to match the real column count — nothing can validate it, and a wrong number silently breaks the grid. Drop an EmptyState size="sm" inside so the block sits in proportion to the rows it replaced.

Agents0
AgentOwnerRuns
No agents yet
Create your first agent and every run it makes shows up here.

Props

PropTypeDefaultDescription
TableCard size"default" | "sm""default"Density for the whole shell — card spacing and table cell padding together. Text size never moves.
TableCardHeaderdiv props—Grid block above the rows. Places the title, the description and the actions without wrappers.
TableCardTitlediv props—CardTitle laid out as a flex row so a count pill can sit beside the text.
TableCardDescriptiondiv props—Optional supporting line under the title.
TableCardCount variantBadge variant"secondary"The count pill beside the title. Not the count variant — that one is filled with --info, and blue here means informational, never a quantity.
TableCardActionsdiv props—Right-hand cluster in the header, spanning both title rows.
TableCardBodydiv props—Zero-padding wrapper for the table, and the positioning anchor TableBulkBar rises from.
TableCardFooterdiv props—Muted strip under the rows. Result count on the left, Pagination on the right with mx-0 w-auto justify-end.
TableBulkBar openboolean—Show the bar. It stays mounted through the exit so the closing animation has something to run on.
TableBulkBar countnumber—Caller-computed selection size. Ticks between values rather than cutting.
TableBulkBar label[string, string]["row", "rows"]Singular and plural noun for the count — ["agent", "agents"].
TableBulkBar onClear() => void—Adds the trailing dismiss button. Omit it and no clear affordance is drawn.
TableBulkBarActionButton propsvariant="ghost" size="sm"A Button retinted for the dark pill — ghost alone keeps its light-surface hover and goes invisible there.

Pesto UI · 117 documented components