Data table
Toolbar, header, rows, pagination — the console's workhorse, rendered as a real table rather than a grid of divs.
Anatomy
| Source | Publisher | Freshness | |||
|---|---|---|---|---|---|
| Cadastre — state parcel fabric | State Government | 02/08/2026 | Fresh | 1,284,901 | |
| City Plan zoning | Riverton City | 31/07/2026 | Fresh | 412,006 | |
| Flood overlay | Riverton City | 14/06/2025 | Stale | 88,420 | |
| Bushfire hazard | State Government | 09/07/2026 | Fresh | 51,733 | |
| Easements register | State Titles Registry | — | Unreachable | 0 |
Code
const COLUMNS: GdColumn[] = [
{ key: 'name', label: 'Source', flex: 1.6 },
{ key: 'sync', label: 'Last sync', width: '110px', mono: true, sortable: true },
{ key: 'rows', label: 'Rows', width: '80px', align: 'end', mono: true, sortable: true }
]
<GdDataTable
v-model:selected="selected"
v-model:sort="sort"
:columns="COLUMNS"
:rows="rows"
row-key="id"
label="Data sources"
selectable
@row-click="open"
>
<template #toolbar>…filter, chips, buttons…</template>
<template #cell-state="{ row }"><StateDot :tone="row.tone" /></template>
<template #footer>
<GdPagination v-model="page" :total="12" :per-page="5" />
</template>
</GdDataTable>Pagination
Code
<GdPagination v-model="page" :total="12" :per-page="5" />API
GdDataTable
Props
columnsrequiredGdColumn[]—The column definitions, in render order. See the GdColumn table below.
rowsrequiredRecord<string, unknown>[]—One page of rows, already sorted and filtered. The table renders what it is given.
labelrequiredstring—The accessible name, rendered as a visually hidden `<caption>`. A table with no caption is an unlabelled region.
rowKeyrequiredstring—The key on each row that is stable across sorts. Selection and Vue's list keying both ride it.
selectableboolean—Renders the 44px selection column and its 16px boxes. The header box selects the page and goes indeterminate at "some".
selected(string | number)[][]Selected row keys, as `v-model:selected`.
sortGdSort | nullnullSort state, as `v-model:sort` — `{ key, dir }`. Clicking a sortable header updates it; the table never reorders the rows itself.
Slots
toolbarAbove the header — filters, search, buttons. The "n selected" count is appended to this row automatically.
toolbar-actionsAfter that count, for actions that only make sense on a selection.
cell-{key}Scoped, one per column key — `{ row, value }`. Without it the cell renders `column.format(row)`, or the raw value.
footerUnder the scroll container. Usually a `GdPagination`.
Events
row-click[row]The row was activated. Clicks in the selection cell do not fire it.
GdColumn
Props
keyrequiredstring—The property read from each row, and the name of the `cell-{key}` slot.
labelrequiredstring—The header text.
flexnumber1A proportional width; omitted, it is treated as 1. Applied to the `<colgroup>` as a pixel hint of flex × 160 — not a percentage.
widthstring—A fixed width — "110px". Wins over `flex`.
align"start" | "end"—`end` right-aligns the column, header included. For figures.
monoboolean—Machine facts: mono face, caption size, tabular numerals, and `data-gd-numeric` so the column does not jitter as it updates.
sortableboolean—The header becomes a real button carrying `aria-sort`. Clicking toggles `v-model:sort` between desc and asc.
format(row) => string—Derive the cell's text from the whole row. Ignored when a `cell-{key}` slot is present.
GdPagination
Props
modelValuerequirednumber—The current page, as `v-model`. One-based, and clamped to the page count.
totalrequirednumber—How many rows there are in total — not on this page. It is what makes the range readout true.
perPagerequirednumber—Rows per page. With `total` it gives both the range readout and the page count.
Usage Guidelines
- Use a table for a set of same-shaped records with more than two attributes worth comparing across rows.
- Use a list row when each item is a subject with a title and a subtitle rather than a set of columns.
- Sort the rows yourself in response to
v-model:sort— usually by asking the server again. - Make the row the link. Handle
@row-click; do not add a trailing "View" button. - Set
monoon dates, identifiers and figures, and leave it off human sentences. - Pass
widthfor a column whose content has a known size, andflexfor the prose ones that should absorb the rest. - Put pagination in the
footerslot and give it the realtotal, not the number of rows on screen.
Behavior & Anatomy
Why a real table
The flex-based row the design draws looks identical and loses everything a table gives an assistive technology. In a real table a screen reader announces "column 3 of 6, Publisher, Riverton City". In a grid of divs it announces "Riverton City", and there is no way to navigate by column at all. The proportional widths come from GdColumn.flex and are applied to a <colgroup>, which keeps the layout without giving up the semantics.
- The row is the link — never a trailing "view" button. Selection rides the 16px checkbox column, whose clicks do not fire the row; the header box selects the page and goes indeterminate at "some".
- Sort is state, not behaviour. A sortable header is a real button carrying
aria-sort, and clicking updatesv-model:sort— the table never reorders the rows itself, because the rows are usually one page of a server's answer, and sorting the page you can see lies about the rest.
Machine facts are monospaced and tabular; human sentences are not. mono: true on a column is what says which is which, and data-gd-numeric stops a column of figures jittering as it updates.
Pagination
The range readout comes first, because it is the only thing that tells a user whether the filter they just applied did anything.
Past seven pages it window-elides rather than rendering forty numbers, and the elision is a static "…" rather than a control: a clickable ellipsis that jumps an arbitrary distance is a worse affordance than the arrows beside it.