DataTable
Hero example
No rows. |
When to use
<DataTable> is Matter's canonical tabular surface. Reach for it wherever a dashboard
needs to render a list of resources — entities in a portfolio, filings in a state,
grants under a plan, stakeholders on a cap table, audit entries on a document. Anywhere
the user thinks "list with columns I can sort + filter."
The primitive is opinionated about geometry (dense hairline rows, 13px sans body, 11px
mono uppercase headers, peach focus ring) and agnostic about row shape — pass any T[]
plus a typed ColumnDef<T>[] and the rest is composition.
const cols: ColumnDef<EntityRow, unknown>[] = […]. The compiler then catches every misaligned accessor + cell renderer at the callsite.<Link href> wrapper around the whole row yet. Use a Link inside the first column's cell renderer. Whole-row href is a planned DataTable feature (see "Follow-ups" below).Anatomy
A <table class="data-table-grid"> with a single header row and per-row body cells.
Header cells (.data-table-th) carry aria-sort and a sort-indicator glyph
(▲ / ▼ / ↕). Body rows (.data-table-row) get a .data-table-row-interactive
modifier when onRowClick is set. State slots (.data-table-state) render
empty / loading / error nodes in place of the row list. Pagination
(.data-table-pagination) lives below the table with a mono info row and
prev / next buttons.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| columnsrequired | ColumnDef<TRow, unknown>[] | — | Column definitions per TanStack Table v8. |
| datarequired | TRow[] | — | Row data. Mutate via state; do not pass a new array literal each render. |
| globalFilter | string | — | Global filter string — case-insensitive substring across all cell values. |
| enableSelection | boolean | — | Enable row-selection checkboxes. Default: false. |
| pageSize | number | null | 25 | Rows per page. Default: 25. Pass null to disable pagination. |
| empty | React.ReactNode | DEFAULT_EMPTY | Empty state — rendered when the (filtered) row count is zero. |
| loading | boolean | false | Loading state — rendered when `loading` is true; shows the skeleton. |
| error | React.ReactNode | — | Error state — rendered as the table body when set. |
| onRowClick | (row: TRow) => void | — | Click handler — receives the row's original data. |
| className | string | — | — |
State slots
DataTable explicitly renders three non-row states:
| Prop | When | Default |
|---|---|---|
empty | Row count after filter is zero | <div class="data-table-empty">No rows.</div> |
loading | Caller passes loading={true} | Loading… text |
error | Caller passes any node as error | Replaces the entire row body |
Sort + filter
Every column is sortable by default — click the header to cycle
none → ascending → descending → none. The
Web Interface Guidelines rule requires aria-sort on
sortable headers; the primitive wires this automatically.
globalFilter is a controlled string prop. Pass a search-input's value and the table
filters case-insensitively across every visible cell. Per-column filters land in a
follow-up.
import { DataTable, type ColumnDef } from "@matter/components";
import { useState } from "react";
type Filing = { id: string; jurisdiction: string; status: "draft" | "filed"; dueIso: string };
const cols: ColumnDef<Filing>[] = [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "jurisdiction", header: "Jurisdiction" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "dueIso", header: "Due" },
];
function FilingsList({ rows }: { rows: Filing[] }) {
const [filter, setFilter] = useState("");
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<DataTable<Filing> columns={cols} data={rows} globalFilter={filter} />
</>
);
}Pagination
Default page size is 25. Pass pageSize={N} to override, or pageSize={null} to
render every row without pagination. Pagination controls hide automatically when there
are zero rows, loading, or in an error state.
Themes
Light / dark / forced-colors side-by-side.
No rows. |
No rows. |
No rows. |
Tokens consumed
Per-row borders use --line-default; row hover tint is --stone-50. Header foreground
reads --fg-muted. Pagination button border uses --line-default; disabled state is
opacity: 0.4. Sort indicators inherit currentColor.
Do / Don't
<Pill tone="green"> for status, <EndpointBadge method=…> for method labels, plain text for everything else.Recipes
This component appears in:
- Transactions list — first canonical consumer (apps/app/(authenticated)/transactions/page.tsx).
- Future: entities list, filings list, grants list, audit timeline.
Follow-ups
DataTable today is the floor primitive. Planned features tracked in the design-system review punch-list:
rowHref?: (row) => string— wraps each row in a Link to preserve cmd-click / right-click navigation while keeping column-cell composition.- Per-column filter UI inside the header dropdown.
- Per-page-size picker (10 / 25 / 50 / 100) inside the pagination strip.
- Row-selection slot (checkbox column + selected-rows callback). The
enableSelectionprop is reserved but unimplemented today.