Skip to content

DataTable

Records as a table: a caption, column headers, a row for each record and, when asked, a way to sort and a link to each record’s page.

import { DataTable } from '@zitdevs/ui';

Sorted by link. Each sortable header links to the order that follows its own, and the first cell of a row links to its page:

Projects
ProjectOwnerStatusHours
Customer portalAdaOn track128
Billing migrationGraceAt risk342
Design systemLinusOn track96
<DataTable
id="projects"
caption="Projects"
columns={[
{ id: 'name', header: 'Project', cell: (row) => row.name, sortable: true, rowHeader: true },
{ id: 'owner', header: 'Owner', cell: (row) => row.owner, sortable: true },
{ id: 'hours', header: 'Hours', cell: (row) => row.hours, align: 'end' },
]}
rows={projects}
rowKey={(row) => row.id}
sort={{ column: 'name', direction: 'ascending' }}
sortHref={(column, direction) => `/projects?sort=${column}:${direction}`}
rowHref={(row) => `/projects/${row.id}`}
/>

Sorted by buttons, dense, with the caption left to assistive technology. The headers are submit buttons of a GET form, so Enter and Space press them:

Projects
StatusHours
Customer portalAdaOn track128
Billing migrationGraceAt risk342
Design systemLinusOn track96
<DataTable
{...table}
density="compact"
captionHidden
sort={{ column: 'owner', direction: 'descending' }}
sortAction="/projects"
sortFields={{ team: 'platform' }}
/>

Empty, and loading:

Closed projects
ProjectOwnerStatusHours

No closed projects

Projects show up here once their last delivery is approved.

Loading projects
ProjectOwnerStatusHours
Loading
<DataTable {...table} rows={[]} empty={{ title: 'No closed projects' }} />
<DataTable {...table} loadingRows={3} />
Prop Type Default Notes
id string required Keys the caption, and the sort form (<id>-sort).
caption string required Says what the table lists. It names the table and its scroll region.
captionHidden boolean false Keeps the caption for assistive technology when a heading above says the same.
columns readonly DataTableColumn<Row>[] required { id, header, cell, sortable?, align?, rowHeader?, hideHeader? }.
rows readonly Row[] required Already in the order to show.
rowKey (row) => string required A stable key, such as the record’s id.
sort { column; direction } — The order the rows arrived in: sets aria-sort on that header.
sortHref (column, direction) => string — Sorts by link. The direction is the one that follows the current order.
sortAction string — Sorts by button instead: a GET form to this address. Ignored when sortHref is set.
sortName string 'sort' The parameter a sort button sends, valued <column>:<direction>.
sortFields Record<string, string> — Hidden fields of the sort form, such as the filters in force.
rowHref (row) => string | undefined — Links the row’s first cell to its page. A record with none is not linked.
linkAs LinkComponent 'a' Renders the links, such as Next’s Link.
density 'comfortable' | 'compact' 'comfortable' Cell padding.
empty EmptyStateProps — Shown, spanning the columns, when there are no rows.
loadingRows number — Shows this many skeleton rows in place of the records, and marks the table aria-busy.
labels Partial<DataTableLabels> English { loading }. es.dataTable from labels/es.
ref Ref<HTMLDivElement> — The scroll region around the table.
className string — Placement, on the region.
anything else <div> attributes — On the region.

What a sortAction form carries. A sort form is a GET form, and a GET form sends only its own fields: the sort value of the button pressed (<column>:<direction>) and the hidden inputs sortFields renders. It does not read the filters or the page from the address. An app that filters or pages the list passes the filters in force and the page through sortFields (sortFields={{ q: 'ada', team: 'platform', page: '1' }}), or the sort would drop them. With sortHref the app builds each address itself, so it keeps them there.

A column’s align: 'end' is for figures, rowHeader reads its cells as the row’s name, and hideHeader keeps the header for assistive technology and takes it off the page, for a column of actions.

Exported types: DataTableProps, DataTableColumn, SortDirection, DataTableLabels, and the label sets dataTableLabels.

  • A real <table> with a <caption>, scope="col" headers and, for a column marked rowHeader, scope="row" cells.
  • aria-sort marks the sorted header. The arrow beside it is decoration.
  • A sortable header is a link or a button named by its header text. Sorting by button uses a form outside the table, so a table inside another form still works.
  • The scroll region is focusable and named by the caption, so a keyboard reaches a table wider than its room.
  • Loading marks the table aria-busy and announces “Loading” once, politely. The grey bars are hidden from assistive technology.
  • Every link and button draws the kit’s focus ring.

It needs none, and renders as a Server Component. Sorting is a link or a GET form: the order lives in the address, so it survives a reload and can be shared. Keep the address the single source of the order and pass it back as sort.

raised, sunken, hover, border-default and border-subtle for the surfaces and lines; ink, body, muted and primary for text; the focus rings, and the inset one on the scroll region.