Data Table
block / data / table
Sortable, searchable, paginated table with in-cell visualisations and every state, on real table semantics. No table engine, no chart library.
- type
- block
- files
- 3
- registry deps
- avatar, badge, button, checkbox, input, skeleton, @automindz/empty-state
- npm deps
- lucide-react
installs
- lib/automindz/table-model.ts
- components/automindz/data-cells.tsx
- components/automindz/data-table.tsx
- Columns are a prop — add one without editing the table
- In-cell bar, donut, sparkline, heat, target and distribution, all inline SVG
- Hover any chart for detail; one tooltip for the whole table, never two
Columns are a prop: value(row) is the one sortable and searchable primitive, cell(row, context) draws it, and context.stats carries that column's max and total over the *filtered* rows — so a bar means the same thing on every page. rows={undefined} renders the skeleton, rows={[]} the empty state, and error the failure with a retry. The lib is table-model.ts, not data-table.ts, because shadcn add rewrites imports across the whole install by basename — do not rename it back. If you installed this item before the rename, delete the leftover lib/automindz/data-table.ts: it still exports the same contract and will silently go stale.
Variations
one state per section
- 01Client accounts, everything live
- 02In-cell visualisations
- 03Sorting
- 04Row selection and bulk actions
- 05Pagination
- 06Compact density
- 07Loading
- 08Nothing created yet
- 09No search results
- 10Failed to load
- 11Long values, horizontal scroll
- 12Narrow container
Client accounts, everything live
Sort any header, search, page through. The client column is the row header (<th scope="row">); numeric columns are right-aligned with tabular figures; every visualisation prints its value beside the mark, so nothing is carried by colour or length alone. Sweep across a sparkline and the readout snaps to the nearest real point with a marker on it; the bar names the column maximum it is scaled against and the donut names the total it is a share of — both invisible on the mark. Only one bubble can exist at a time, however fast you sweep.
| LRLumen Retail | Active | $15,300, 100% of the $15,300 column maximum |
|---|---|---|
| HHyreflow | Active | $12,780, 84% of the $15,300 column maximum |
| MHMeridian Health | Active | $9,150, 60% of the $15,300 column maximum |
| SMSolstice Media | Active | $8,470, 55% of the $15,300 column maximum |
| O&Orchard & Co | Active | $5,640, 37% of the $15,300 column maximum |
| NLNorthwind Labs | Active | $4,860, 32% of the $15,300 column maximum |
Showing 1–6 of 14 rows
In-cell visualisations
Heat cell (five binned steps of one hue), progress toward a target with the target marked by a tick rather than implied, and a channel breakdown whose sixth category folds into “Other” — there are five chart tokens, so a sixth colour is never invented. Hover any of the three: the heat cell names the band it landed in, the target bar gives the percentage attained, and the traffic mix names the segment under the pointer, including what “Other” swallowed. Every one of those strings is in the cell's sr-only text too, so the tooltip is a convenience and never the only route to a value.
| Traffic mix, share of sessions by channel | |||
|---|---|---|---|
| HHyreflow | 512.9K, band 5 of 5: 410.3K to 512.9K | 48 / 60 seats, 80% of the target | Organic 52%Organic 52%, Paid 18%, Referral 16%, Email 7%, Other 7% (Social, Direct) |
| MHMeridian Health | 288.7K, band 3 of 5: 205.2K to 307.7K | 34 / 60 seats, 57% of the target | Organic 61%Organic 61%, Referral 13%, Email 11%, Paid 9%, Other 6% (Social, Direct) |
| BHBright Harbour | 141K, band 2 of 5: 102.6K to 205.2K | 19 / 20 seats, 95% of the target | Organic 33%Organic 33%, Paid 27%, Referral 19%, Email 12%, Other 9% (Social, Direct) |
| NLNorthwind Labs | 128.4K, band 2 of 5: 102.6K to 205.2K | 17 / 20 seats, 85% of the target | Organic 46%Organic 46%, Paid 22%, Referral 14%, Email 9%, Other 9% (Social, Direct) |
| CCastUK | 96.2K, band 1 of 5: 0 to 102.6K | 12 / 20 seats, 60% of the target | Organic 38%Organic 38%, Paid 31%, Email 12%, Referral 11%, Other 8% (Social, Direct) |
| KLKestrel Logistics | 12.4K, band 1 of 5: 0 to 102.6K | 5 / 5 seats, 100% of the target | Organic 41%Organic 41%, Referral 22%, Paid 19%, Email 9%, Other 9% (Social, Direct) |
| AFAtlas Freight | 8,100, band 1 of 5: 0 to 102.6K | 4 / 5 seats, 80% of the target | Paid 44%Paid 44%, Organic 29%, Email 10%, Referral 8%, Other 9% (Social, Direct) |
Sorting
Each header cycles ascending → descending → unsorted, and writes aria-sort as it goes. Sort by spend: Fernwood Estates has no spend, and a missing value stays last in both directions — it is not a small number.
| AFAtlas Freight | Trialing | $490, 3% of the $15,300 column maximum |
|---|---|---|
| KLKestrel Logistics | Active | $780, 5% of the $15,300 column maximum |
| BHBright Harbour | Past due | $4,120, 27% of the $15,300 column maximum |
| MHMeridian Health | Active | $9,150, 60% of the $15,300 column maximum |
| HHyreflow | Active | $12,780, 84% of the $15,300 column maximum |
| LRLumen Retail | Active | $15,300, 100% of the $15,300 column maximum |
| FEFernwood Estates | Canceled | — |
Row selection and bulk actions
The header checkbox selects the current page and goes indeterminate on a partial selection. The bulk bar sits above the table rather than floating over the rows, and the checkbox is its own cell — never a control nested inside another.
2 selected
| NLNorthwind Labs | Active | $4,860, 38% of the $12,780 column maximum | |
|---|---|---|---|
| CCastUK | Active | $3,240, 25% of the $12,780 column maximum | |
| HHyreflow | Active | $12,780, 100% of the $12,780 column maximum | |
| AFAtlas Freight | Trialing | $490, 4% of the $12,780 column maximum | |
| MHMeridian Health | Active | $9,150, 72% of the $12,780 column maximum | |
| BHBright Harbour | Past due | $4,120, 32% of the $12,780 column maximum |
Pagination
Four rows a page. Bar lengths and donut shares are computed over the filtered rows, not the page, so a bar means the same thing on page 1 and page 4. Search while on page 3 and the page resets instead of showing an empty body.
| NLNorthwind Labs | Active | $4,860, 32% of the $15,300 column maximum |
|---|---|---|
| CCastUK | Active | $3,240, 21% of the $15,300 column maximum |
| HHyreflow | Active | $12,780, 84% of the $15,300 column maximum |
| AFAtlas Freight | Trialing | $490, 3% of the $15,300 column maximum |
Showing 1–4 of 14 rows
Compact density
density="compact" tightens the row box and drops the type a step. Same columns, same cells, ~35% more rows in the same height — which is the point of a table.
| LRLumen Retail | Active | $15,300, 100% of the $15,300 column maximum |
|---|---|---|
| HHyreflow | Active | $12,780, 84% of the $15,300 column maximum |
| MHMeridian Health | Active | $9,150, 60% of the $15,300 column maximum |
| SMSolstice Media | Active | $8,470, 55% of the $15,300 column maximum |
| O&Orchard & Co | Active | $5,640, 37% of the $15,300 column maximum |
| NLNorthwind Labs | Active | $4,860, 32% of the $15,300 column maximum |
| BHBright Harbour | Past due | $4,120, 27% of the $15,300 column maximum |
| VSVantage Studios | Active | $3,980, 26% of the $15,300 column maximum |
| CCastUK | Active | $3,240, 21% of the $15,300 column maximum |
| KLKestrel Logistics | Active | $780, 5% of the $15,300 column maximum |
Showing 1–10 of 14 rows
Loading
rows={undefined} renders skeleton rows built from each column's skeleton shape — avatar, pill, chart, number — so the placeholder row is the height of the real one and nothing jumps when the data lands.
Nothing created yet
rows={[]} with no query. Reuses @automindz/empty-state inside the table body, so the header stays put and the message is the first-run one: name the thing, give one action.
No client accounts yet Add a client to start tracking spend, seats and API usage. | ||||
No search results
A different message from “nothing exists”, because the fix is different: clear the search. Seeded with defaultQuery so the state is here on arrival.
No rows match “invoice” Try a shorter query, or clear the search. | ||||
Failed to load
The failure outranks the skeleton — with an error there is nothing coming. Rendered with role="alert", the reason spelled out, and a retry that stays busy while it runs. Press it: this one succeeds.
Could not load this table The usage service timed out after 30 seconds. | ||||
Long values, horizontal scroll
The renewal note truncates with the full string in title, and minWidth makes the table wider than the frame: it scrolls inside its own container and the page never scrolls sideways.
| Traffic mix, share of sessions by channel | ||||||
|---|---|---|---|---|---|---|
| NLNorthwind Labs | Growth plan renews 14 Sep 2026 — billing contact [email protected] | Growth | Active | $4,860, 38% of the $12,780 column maximum | 17 / 20 seats, 85% of the target | Organic 46%Organic 46%, Paid 22%, Referral 14%, Email 9%, Other 9% (Social, Direct) |
| CCastUK | Growth plan renews 02 Oct 2026 — billing contact [email protected] | Growth | Active | $3,240, 25% of the $12,780 column maximum | 12 / 20 seats, 60% of the target | Organic 38%Organic 38%, Paid 31%, Email 12%, Referral 11%, Other 8% (Social, Direct) |
| HHyreflow | Scale plan renews 28 Aug 2026 — billing contact [email protected] | Scale | Active | $12,780, 100% of the $12,780 column maximum | 48 / 60 seats, 80% of the target | Organic 52%Organic 52%, Paid 18%, Referral 16%, Email 7%, Other 7% (Social, Direct) |
| AFAtlas Freight | Starter plan renews 31 Aug 2026 — billing contact [email protected] | Starter | Trialing | $490, 4% of the $12,780 column maximum | 4 / 5 seats, 80% of the target | Paid 44%Paid 44%, Organic 29%, Email 10%, Referral 8%, Other 9% (Social, Direct) |
| MHMeridian Health | Scale plan renews 19 Nov 2026 — billing contact [email protected] | Scale | Active | $9,150, 72% of the $12,780 column maximum | 34 / 60 seats, 57% of the target | Organic 61%Organic 61%, Referral 13%, Email 11%, Paid 9%, Other 6% (Social, Direct) |
| BHBright Harbour | Growth plan renews 07 Sep 2026 — billing contact [email protected] | Growth | Past due | $4,120, 32% of the $12,780 column maximum | 19 / 20 seats, 95% of the target | Organic 33%Organic 33%, Paid 27%, Referral 19%, Email 12%, Other 9% (Social, Direct) |
Narrow container
Held at 360px to show the container query: hideBelow columns drop out on the table's own width, not the viewport's, so the same table works in a sidebar and full-bleed. The donut stays circular and the bars shorten instead of distorting.
| HHyreflow | Active | $12,780, 100% of the $12,780 column maximum |
|---|---|---|
| MHMeridian Health | Active | $9,150, 72% of the $12,780 column maximum |
| NLNorthwind Labs | Active | $4,860, 38% of the $12,780 column maximum |
| CCastUK | Active | $3,240, 25% of the $12,780 column maximum |
| AFAtlas Freight | Trialing | $490, 4% of the $12,780 column maximum |
