Components
Data table
View in FigmaA semantic, sortless table driven by flat columns + rows props. Each column owns a cell render function, so status pills, trend numbers, and sparklines compose into cells while the table stays a thin, opinionated grid.
Install
npx shadcn@latest add @alix/dataTablePreview
| Engagement | Margin | MoM | Health |
|---|---|---|---|
| Project Atlas | 31.4% | +1.2pt | On track |
| European auto OEM | 24.8% | -0.6pt | At risk |
| US regional bank | 19.2% | -2.1pt | Off track |
Usage
<DataTable
columns={[
{ key: "client", header: "Client", cell: (r) => r.client, cellClassName: "font-medium" },
{ key: "margin", header: "Margin", numeric: true, cell: (r) => `${r.margin}%` },
{ key: "mom", header: "MoM", numeric: true, cell: (r) => <Delta value={r.mom} direction={r.momDirection} intent={r.momIntent} /> },
{ key: "status", header: "Health", cell: (r) => <Badge variant={r.tone} dot>{r.status}</Badge> },
]}
rows={engagements}
/>Examples
01
Dense financial rows
Dense padding keeps recurring numeric ledgers scannable without changing the column contract.
| Practice | Q1 | Q2 | Margin |
|---|---|---|---|
| Turnaround | $42.8M | $45.1M | 31.4% |
| Performance | $36.2M | $38.7M | 28.9% |
| Transactions | $29.5M | $33.2M | 24.8% |
<div className="w-full max-w-xl border border-border">
<DataTable
caption="Practice margin by quarter"
dense
rows={[
{ practice: "Turnaround", q1: "$42.8M", q2: "$45.1M", margin: "31.4%" },
{ practice: "Performance", q1: "$36.2M", q2: "$38.7M", margin: "28.9%" },
{ practice: "Transactions", q1: "$29.5M", q2: "$33.2M", margin: "24.8%" },
]}
columns={[
{ key: "practice", header: "Practice", cell: (row) => row.practice, cellClassName: "font-medium" },
{ key: "q1", header: "Q1", numeric: true, cell: (row) => row.q1 },
{ key: "q2", header: "Q2", numeric: true, cell: (row) => row.q2 },
{ key: "margin", header: "Margin", numeric: true, cell: (row) => row.margin },
]}
/>
</div>02
Sticky panel ledger
A flush Panel plus stickyHeader gives dense operational tables a stable dashboard frame.
Weekly delivery ledger
Priority workstreams
| Workstream | Owner | Health | Trend |
|---|---|---|---|
| Liquidity forecast | A. Ward | On track | |
| Supplier review | M. Khan | At risk | |
| Board materials | R. Silva | On track | |
| Cost baseline | L. Chen | Off track |
<Panel title="Weekly delivery ledger" description="Priority workstreams" flush>
<div className="max-h-72 overflow-auto">
<DataTable
caption="Weekly delivery ledger"
stickyHeader
rows={[
{ workstream: "Liquidity forecast", owner: "A. Ward", status: "On track", tone: "success" as const, trend: [12, 14, 13, 18, 19, 22] },
{ workstream: "Supplier review", owner: "M. Khan", status: "At risk", tone: "warning" as const, trend: [18, 17, 16, 14, 13, 12] },
{ workstream: "Board materials", owner: "R. Silva", status: "On track", tone: "success" as const, trend: [8, 10, 11, 13, 15, 18] },
{ workstream: "Cost baseline", owner: "L. Chen", status: "Off track", tone: "destructive" as const, trend: [20, 18, 17, 15, 11, 9] },
]}
columns={[
{ key: "workstream", header: "Workstream", cell: (row) => row.workstream, cellClassName: "font-medium" },
{ key: "owner", header: "Owner", cell: (row) => row.owner },
{
key: "status",
header: "Health",
cell: (row) => <Badge variant={row.tone} dot size="sm">{row.status}</Badge>,
},
{
key: "trend",
header: "Trend",
align: "right",
width: "8rem",
cell: (row) => <Sparkline data={row.trend} height={28} />,
},
]}
/>
</div>
</Panel>03
Empty filtered result
DataTable does not own empty copy; pair the same frame with EmptyState when filters remove every row.
Filtered engagements
No matching engagements
Clear filters or widen the reporting period to bring rows back.
const filteredRows: Row[] = [];
<Panel title="Filtered engagements">
{filteredRows.length > 0 ? (
<DataTable caption="Filtered engagements" columns={columns} rows={filteredRows} />
) : (
<EmptyState
title="No matching engagements"
description="Clear filters or widen the reporting period to bring rows back."
action={<Button variant="outline">Clear filters</Button>}
/>
)}
</Panel>When to use it
Use it for
- Sortless dashboard ledgers where rows are already curated by the product or query.
- Cells that need composed status, trend, owner, or progress UI while retaining semantic table markup.
- Dense financial, delivery, or people data where numeric columns must align cleanly.
Reach for something else
- Single KPI summaries — use StatCard instead.
- Visual trend-only cells without row comparison — use Sparkline inside StatCard instead.
- The empty state after filtering — use EmptyState inside the same Panel frame.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | Column<T>[] | — | Column defs, left to right. Column<T> = { key, header, cell: (row, i) => ReactNode, align?, numeric?, width?, cellClassName? }. |
| rows | T[] | — | Row records. Each is passed to every column's cell renderer. |
| numeric (per column) | boolean | false | Right-aligns the column and uses tabular figures, so numbers line up. |
| caption | ReactNode | — | Visually-hidden <caption> describing the table, for screen readers. |
| dense | boolean | false | Tighter row padding for dense data. |
| stickyHeader | boolean | false | Stick the header to the top while the body scrolls. |