DataGrid
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.
| Name | Role | Joined | |
|---|---|---|---|
| Ada Lovelace | Engineer | 2021 | |
| Grace Hopper | Admiral | 1943 | |
| Alan Turing | Mathematician | 1936 |
Show codeHide 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;sortablecolumns get a button with a three-state chevron (unsorted / ascending / descending) andaria-sort. - Select-all checkbox — header cell, shown when
selectable; aCheckboxwired to an indeterminate state. - Body rows — one
<tr>per item inrows; a checked row’s<tr>tints (bg-neutral-faded). - Row checkbox — shown per row when
selectable; aCheckbox.
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):
| Name | Score |
|---|---|
| Charlie | 88 |
| Alice | 95 |
| Bob | 72 |
Show codeHide 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:
| Left | Center | Right |
|---|---|---|
| aligned | aligned | 42 |
Show codeHide 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:
| Name | Seats |
|---|---|
| Larkspur | 12 |
| Fernhill | 4 |
| Brightwell | 30 |
Show codeHide code
<DataGrid size="medium" striped columns={…} rows={…} />Small caps headers, for dense dashboards:
| Name | Seats |
|---|---|
| Larkspur | 12 |
| Fernhill | 4 |
Show codeHide 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-sortand the sort button’sdata-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:
| Item | Qty | |
|---|---|---|
| Widget | 12 | |
| Gadget | 4 |
Show codeHide 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
Checkboxsets the nativeindeterminateproperty 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
keystable and unique — the client sort matches rows by it. - Don’t put rich interactive content in a
sortablecell — sort compares the cell’s text/number value, not markup. - Don’t rely on
selectablealone 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 inherittabular-numsautomatically.