# Data Table

> A dense table on Table: sortable columns, people through Person, statuses, peeks, your row, zones, breaks, and quick actions per row.

- Name: `data-table`
- Status: beta
- Categories: data, display
- File: the install writes `data-table.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/motion`, `@arbokui/hover-card`, `@arbokui/hover-actions`, `@arbokui/motion-timing`, `@arbokui/peek-card`, `@arbokui/person`, `@arbokui/row-actions`, `@arbokui/skeleton`, `@arbokui/status-text`, `@arbokui/table`. The install adds them.
- Gallery: https://arbokui.com/components/data-table
- Registry item: https://arbokui.com/r/data-table.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/data-table
   ```

## Use it for

- Rows of records read across: a team, payments, attendance. Put it in a Card with className="px-0" on the part that holds it, so it runs edge to edge.
- It is generic over the row: every callback sees your row type. Keep columns stable, outside the component or in useMemo, so a sort is not redone each render.
- A field shaped { person, photo } draws as Person. Use render for anything else.
- status puts the row's status right after a cell's value, usually the name. A table's status follows the name, never its own column.
- peek explains a value: who, when, why. Plain text gets a dotted underline.
- sortable adds a header button. The first press sorts text A to Z and numbers high to low, and the next press flips it. aria-sort says which.
- highlight marks your rows with a You tag and their own wash. zones tint a run of rows, like the top places, with a 2px rule where it meets the rest. breaks add a labeled line, like Your place.
- rowActions spring in over the row's end on hover or focus, and their keys work then. If they would cover a peek or a status, the table keeps a band at the end for them.
- hideBelow drops a column while the table is narrow, so the names keep their room.

## Avoid it for

- A list of things that are not read across. Use ListRow.
- Sorting a table with zones or breaks. They follow row positions, so a sort moves them onto other rows.
- A column for the status. Put status on the name column.
- Colors alone for a zone. Say what it means in a peek or in words.

## Props

- `columns`: `DataColumn<Row>[]`. Required. Each: { key, label, short?, width?, align?, numeric?, render?(row, hovered), peek?(row), underline?, status?(row), rowHeader?, sortable?, sortValue?(row), hideBelow?, className? }. short shows in place of label, like "P" for Played, while label is read.
- `rows`: `Row[]`. Required. Long tables draw 100 rows at a time as the reader nears the end.
- `rowKey`: `string | ((row, index) => string | number)`. A stable key per row. Default row.id, then the position.
- `breaks`: `{ after: number; label: string; tone?; side?: "above" | "below" }[]`. A labeled line after a row index. Its arrow says whether the label is about the rows above or below.
- `zones`: `{ from: number; to: number; tone? }[]`. Runs of row indices tinted in a tone as one band, with a 2px rule where the band meets the rest.
- `highlight`: `(row) => boolean`. Rows that are yours: a You tag after the name and their own wash, or the tag alone inside a zone.
- `highlightLabel`: `string`. Default `"You"`. The tag on a highlighted row, which screen readers hear before its name.
- `rowActions`: `(row) => HoverAction[]`. Quick actions over the row's end while it is hovered or holds focus.
- `onRowClick`: `(row) => void`. Makes each row one press, named by its row header.
- `sort`: `{ key: string; direction: "ascending" | "descending" } | null`. Controlled sort.
- `defaultSort`: `{ key: string; direction: "ascending" | "descending" } | null`. The first sort, uncontrolled.
- `onSortChange`: `(sort) => void`. Hears each sort a header button asks for.
- `caption`: `ReactNode`. Names the table, under it.
- `aria-label`: `string`. Names the table when it has no caption.
- `loading`: `boolean`. Default `false`. Holds the space with three placeholder rows.
- `empty`: `ReactNode`. Default `"Nothing here yet"`. What shows with no rows.
- `data-slot`: `string`. Default `"data-table"`. The root's slot, for a part built on DataTable, like Leaderboard.
- `className`: `string`. Extra classes on the wrapper. Set --data-table-edge to change the first and last columns' inset. Later classes win.

## Accessibility

- A real \<table>, with column headers. A column marked rowHeader is a \<th scope="row">, so each cell is read with the row's name.
- A sortable header is a button inside the \<th>. The sorted header carries aria-sort, ascending or descending.
- With onRowClick, each row's press is one real button named by its row header. Its focus ring goes around the whole row. Controls in the row keep their own presses, and none sits inside another.
- A highlighted row's row header starts with "You," for screen readers, so every cell is read with it. Its tag is words, so it stays in forced colors.
- A zone's edges are 2px borders, so they stay in forced colors. Its meaning needs words, like a peek on its rows.
- Row actions show on hover and on focus inside the row. Their single-key shortcuts work only while the row is lit.
- Each row carries data-peek-row, so a peek opens beside it, and ↑↓ step between peeks.
- While loading, the table is aria-busy.

## Examples

### Sortable columns with people

```tsx
type Member = { id: string; name: { person: string }; role: string; projects: number };

const columns: DataColumn<Member>[] = [
  { key: "name", label: "Name", rowHeader: true, sortable: true, sortValue: (row) => row.name.person },
  { key: "role", label: "Role" },
  { key: "projects", label: "Projects", numeric: true, sortable: true },
];

<DataTable
  aria-label="Team"
  columns={columns}
  rows={[
    { id: "p1", name: { person: "Tom Hale" }, role: "Designer", projects: 42 },
    { id: "p2", name: { person: "Josh Tait" }, role: "Engineer", projects: 17 },
  ]}
/>
```
