Skip to content
Components

Data table

Toolbar, header, rows, pagination — the console's workhorse, rendered as a real table rather than a grid of divs.

Anatomy

Publisher: Riverton City 1 selected
Data sources
SourcePublisherFreshness
Cadastre — state parcel fabricState Government02/08/2026 Fresh1,284,901
City Plan zoningRiverton City31/07/2026 Fresh412,006
Flood overlayRiverton City14/06/2025 Stale88,420
Bushfire hazardState Government09/07/2026 Fresh51,733
Easements registerState Titles Registry— Unreachable0
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

PropTypeDefault
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 | nullnull

Sort state, as `v-model:sort` — `{ key, dir }`. Clicking a sortable header updates it; the table never reorders the rows itself.

Slots

toolbar

Above the header — filters, search, buttons. The "n selected" count is appended to this row automatically.

toolbar-actions

After 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.

footer

Under 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

PropTypeDefault
keyrequiredstring—

The property read from each row, and the name of the `cell-{key}` slot.

labelrequiredstring—

The header text.

flexnumber1

A 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

PropTypeDefault
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 mono on dates, identifiers and figures, and leave it off human sentences.
  • Pass width for a column whose content has a known size, and flex for the prose ones that should absorb the rest.
  • Put pagination in the footer slot and give it the real total, 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 updates v-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.

Navigate

Esc