Skip to content
ComponentsDataTable

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.

Do — type your columns explicitly: const cols: ColumnDef<EntityRow, unknown>[] = […]. The compiler then catches every misaligned accessor + cell renderer at the callsite.
Don't — build a per-row <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

PropTypeDefaultDescription
columnsrequiredColumnDef<TRow, unknown>[]Column definitions per TanStack Table v8.
datarequiredTRow[]Row data. Mutate via state; do not pass a new array literal each render.
globalFilterstringGlobal filter string — case-insensitive substring across all cell values.
enableSelectionbooleanEnable row-selection checkboxes. Default: false.
pageSizenumber | null25Rows per page. Default: 25. Pass null to disable pagination.
emptyReact.ReactNodeDEFAULT_EMPTYEmpty state — rendered when the (filtered) row count is zero.
loadingbooleanfalseLoading state — rendered when `loading` is true; shows the skeleton.
errorReact.ReactNodeError state — rendered as the table body when set.
onRowClick(row: TRow) => voidClick handler — receives the row's original data.
classNamestring

State slots

DataTable explicitly renders three non-row states:

PropWhenDefault
emptyRow count after filter is zero<div class="data-table-empty">No rows.</div>
loadingCaller passes loading={true}Loading… text
errorCaller passes any node as errorReplaces 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

No tokens match.

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

Do — compose cell renderers with V2 primitives: <Pill tone="green"> for status, <EndpointBadge method=…> for method labels, plain text for everything else.
Don't — re-author table chrome (borders, hover, sort affordance) per surface. The primitive owns the grammar; consumers pick columns.

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 enableSelection prop is reserved but unimplemented today.

Source

packages/components/src/DataTable

On this page