Table · Pagination Composition

A table, not a data grid: it renders rows and sorts by a column. No virtualisation, no column resizing, no inline editing — those are product features, and a design system that grows them becomes a framework nobody can leave.

Usage

Status
INV-1045
Citra Dewi
pending2026-08-09Rp 2.300.000
INV-1044
Budi Santoso
paid2026-08-06Rp 640.000
INV-1042
Rio Hakim
pending2026-08-03Rp 1.120.000
INV-1041
Ada Putri
paid2026-08-01Rp 4.250.000
INV-1043
Sarah Chen
overdue2026-07-18Rp 9.800.000
tsx
const [sort, setSort] = useState<SortState | null>({ column: 'issued', direction: 'desc' });
<Table  label="Invoices"  columns={columns}  rows={rows}  rowKey={(row) => row.id}  sort={sort}  onSortChange={setSort}  empty={<Empty title="No invoices yet" />}/>
<Pagination page={page} pageSize={20} total={137} onPageChange={setPage} />

Sorting has three states

Clicking a new column sorts ascending, clicking again flips it, and clicking a third time clears the sort. That third state matters: "put it back the way the server gave it to me" is something people look for, and two-state sorting hides it.

The state lives on the header as aria-sort, not only in the icon — a screen reader announces the sort without seeing the arrow.

Do

Keep sorting controlled: the table reports what was clicked and your data layer decides what that means.

Don't

Sort inside the table over a paginated set — you would only be sorting the page in view.

Pagination

tsx
<Pagination page={page} pageSize={10} total={300} onPageChange={setPage} />

The page list always keeps the first and last page reachable, and a single skipped page is spelled out rather than hidden behind an ellipsis that costs the same width. An empty set reads 0–0 of 0 rather than 1–0.

Props

PropTypeDefaultNotes
columnsArray<Column<Row>>key, header, cell(row), align, width, sortable, secondary.
rows / rowKeyRow[] / (row) => stringA stable key per row; index keys break every time the sort changes.
labelstringRequired accessible name for the table.
sort / onSortChangeSortState | nullControlled. nextSort() is exported if you drive it yourself.
onRowClick(row) => voidMakes rows clickable; keep a real control in the row too, for keyboards.
emptyReactNodeReplaces the table entirely when there are no rows — an Empty fits here.
Column.secondarybooleanHides the column below 720px, for what a phone can live without.
Column.truncatebooleanClips overflowing text with an ellipsis instead of letting one long value widen the column.