A data table with client-side column sorting and optional row selection.

Last updated

Uses JavaScript

Wires client-side column sorting (numbers sort numerically, everything else as text) and, when selectable, row checkboxes with an indeterminate select-all.

Without JS: Rows render in the order given by the rows prop. Sortable headers show their plain label instead of a button and the select-all checkbox is hidden (`no-js:hidden`), so no dead controls appear; per-row checkboxes stay as ordinary form controls.

Role
Ada LovelaceEngineer2021
Grace HopperAdmiral1943
Alan TuringMathematician1936
Show code
<DataGrid
selectable
columns={[
  { key: 'name', label: 'Name', sortable: true },
  { key: 'role', label: 'Role' },
  { key: 'joined', label: 'Joined', sortable: true, align: 'end' },
]}
rows={[
  { name: 'Ada Lovelace', role: 'Engineer', joined: 2021 },
  { name: 'Grace Hopper', role: 'Admiral', joined: 1943 },
  { name: 'Alan Turing', role: 'Mathematician', joined: 1936 },
]}
/>

Add

npx @ava-ui/apogee add data-grid

Pulls in Checkbox (row selection). Ships one scoped JS island. Uses @lucide/astro (ChevronsUpDown, ChevronUp, ChevronDown sort icons).

Usage

---
import DataGrid from '@/components/apogee/DataGrid/DataGrid.astro'
---
<DataGrid
  columns={[{ key: 'name', label: 'Name' }]}
  rows={[{ name: 'Ada Lovelace' }]}
/>

Anatomy

  • Header row — one <th> per column; sortable columns get a button with a three-state chevron (unsorted / ascending / descending) and aria-sort.
  • Select-all checkbox — header cell, shown when selectable; a Checkbox wired to an indeterminate state.
  • Body rows — one <tr> per item in rows; a checked row’s <tr> tints (bg-neutral-faded).
  • Row checkbox — shown per row when selectable; a Checkbox.

API

@composable — exposed to Content Editors in AVA. Any other attribute passes through to the root wrapper <div>.

Prop Type Default Description
columns Column[] — Column definitions, in order.
rows Record<string, any>[] — Row data; each row’s keys match columns[].key.
selectable boolean false Adds a checkbox column with an indeterminate select-all.
size 'small' | 'medium' 'small' Cell padding, matching Table.
striped boolean false Alternate row fills.
headerStyle 'normal' | 'caps' 'normal' Header text: sentence case and semibold, or small caps.
class string — Extra classes on the wrapper, merged last.

Column shape

Field Type Default Description
key string — Key read from each row’s data.
label string — Column header text.
sortable boolean false Adds a sort button to the header.
align 'start' | 'end' | 'center' 'start' Header + cell text alignment (logical, mirrors in RTL).

Variants

Sortable columns (click a header to cycle unsorted → ascending → descending):

Charlie88
Alice95
Bob72
Show code
<DataGrid
columns={[
  { key: 'name', label: 'Name', sortable: true },
  { key: 'score', label: 'Score', sortable: true, align: 'end' },
]}
rows={[
  { name: 'Charlie', score: 88 },
  { name: 'Alice', score: 95 },
  { name: 'Bob', score: 72 },
]}
/>

Column alignment — start, end, center:

LeftCenterRight
alignedaligned42
Show code
<DataGrid
columns={[
  { key: 'a', label: 'Left', align: 'start' },
  { key: 'b', label: 'Center', align: 'center' },
  { key: 'c', label: 'Right', align: 'end' },
]}
rows={[{ a: 'aligned', b: 'aligned', c: 42 }]}
/>

Medium density with stripes:

Seats
Larkspur12
Fernhill4
Brightwell30
Show code
<DataGrid size="medium" striped columns={…} rows={…} />

Small caps headers, for dense dashboards:

Seats
Larkspur12
Fernhill4
Show code
<DataGrid headerStyle="caps" columns={…} rows={…} />

For rich cells (avatars, badges) with sorting, use Table with sortable.

States

  • Unsorted / ascending / descending — the header chevron swaps to match aria-sort and the sort button’s data-sort.
  • Row checked — the row background tints.
  • Indeterminate select-all — shown when some, but not all, rows are checked.
  • No JS — see the JsNote above.

Examples

Selectable rows, numeric column right-aligned with tabular-nums:

ItemQty
Widget12
Gadget4
Show code
<DataGrid
selectable
columns={[
  { key: 'item', label: 'Item' },
  { key: 'qty', label: 'Qty', align: 'end' },
]}
rows={[
  { item: 'Widget', qty: 12 },
  { item: 'Gadget', qty: 4 },
]}
/>

Accessibility

Key Action
Tab Moves through the select-all checkbox, sort buttons and row checkboxes.
Enter Space Sorts by the focused column.
Space Toggles the focused checkbox.
  • Semantic <table> / <thead> / <tbody>.
  • Each sortable header keeps aria-sort (none / ascending / descending) in sync with the active sort.
  • The select-all Checkbox sets the native indeterminate property when some but not all rows are checked.
  • Checkboxes carry aria-label="Select all" / "Select row".
  • No JS — rows render in the order given by rows; sort buttons and the select-all checkbox render but are inert. Nothing is hidden, so the data itself is always readable.

Tokens

Token Used for
--border-neutral Wrapper border, header/row dividers
--background-neutral-faded Header background, checked-row tint
--foreground-neutral-faded Header label color, inactive sort icon
--foreground-neutral Cell text, hovered sort label

Do / Don’t

  • Do mark only genuinely sortable columns sortable — a chevron implies a working sort.
  • Do keep each column’s key stable and unique — the client sort matches rows by it.
  • Don’t put rich interactive content in a sortable cell — sort compares the cell’s text/number value, not markup.
  • Don’t rely on selectable alone for bulk actions — pair it with a visible action bar that reacts to the checked rows.

Content

  • Column labels are short nouns, sentence case, no ending punctuation.
  • Numeric columns end-align (align="end") and inherit tabular-nums automatically.