Data display
Numbers first, then the records behind them: a stat-card row above a data table. Numerals are mono with tabular figures — everything that compares must line up.
Stat cards
StatCard is a composition, not an export — a bordered card with a mono figure and an
eyebrow label:
import { Card, CardContent } from '@elirobinson/react/components/molecules/Card';
export function StatCard({
label,
value,
delta,
}: {
label: string;
value: string;
delta?: string;
}) {
return (
<Card>
<CardContent className="stat-card">
<p className="stat-card__label">{label}</p>
<p className="stat-card__value">{value}</p>
{delta && <p className="stat-card__delta">{delta}</p>}
</CardContent>
</Card>
);
}.stat-card__label {
font-family: var(--font-mono);
font-size: var(--fs-xs);
letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--fg-3);
margin: 0 0 var(--space-2);
}
.stat-card__value {
font-family: var(--font-mono);
font-size: var(--fs-3xl);
font-weight: var(--fw-semibold);
font-variant-numeric: tabular-nums;
margin: 0;
}
.stat-card__delta {
font-family: var(--font-mono);
font-size: var(--fs-sm);
color: var(--status-success);
margin: var(--space-1) 0 0;
}Lay them out in an asymmetric grid — a 2fr card for the headline number beats four equal boxes.
The records
Below the stats, reach for Table — paginated, sortable,
filterable — or VirtualTable once row counts get into the
thousands. Both take the same TanStack ColumnDef columns, so switching later is a
rename. Wire the empty case to EmptyState — Table renders
one automatically via emptyMessage.
import { Table } from '@elirobinson/react/components/organisms/Table';
import type { ColumnDef } from '@elirobinson/react/components/organisms/Table';
type Order = { id: string; guide: string; amount: string; date: string };
const columns: ColumnDef<Order>[] = [
{ accessorKey: 'id', header: 'Order' },
{ accessorKey: 'guide', header: 'Guide' },
{ accessorKey: 'amount', header: 'Amount' },
{ accessorKey: 'date', header: 'Date' },
];
export function OrdersTable({ orders }: { orders: Order[] }) {
return (
<Table
data={orders}
columns={columns}
pageSize={10}
emptyMessage="No orders yet — share your store link."
/>
);
}Rules
- Mono numerals with
tabular-numsanywhere numbers stack: stats, amounts, quantities. - Zebra rows use
--bg-subtle, hairline row dividers use--border— never both heavier. - Right-align numeric columns, left-align text; headers match their column.
- Deltas:
--status-successup,--status-dangerdown, plain--fg-3for flat — and the sign travels with the number ("+12%"), color is never the only signal. - An empty table is a first-run experience, not an error — give the EmptyState a next step.