# DataGrid

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

URL: https://apogee.avaui.dev/docs/data-grid
Last updated: 2026-10-16

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

Last updated October 16, 2026

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.

```astro
<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

```sh
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

```astro
---
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):

```astro
<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`:

```astro
<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:

```astro
<DataGrid size="medium" striped columns={…} rows={…} />
```

Small caps headers, for dense dashboards:

```astro
<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`:

```astro
<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.

## Related

[

TableA styled, horizontally-scrollable table. Write plain thead/tbody markup — Table only styles it.

](https://apogee.avaui.dev/docs/table)[

ListA styled ordered or unordered list for grouped item content — not for layout rows.

](https://apogee.avaui.dev/docs/list)[

PaginationPage links with prev/next and ellipses for long page ranges.

](https://apogee.avaui.dev/docs/pagination)
