# Table

> The one table look: neutral header row, whole-cell sortable headers, skeleton, empty and error states, and Pagination.

Category: Data display. Docs page: https://rime.mjsons.net/components/table

## Installation

```bash
npx shadcn@latest add KezLahd/rime/table
```

Table is presentational: you own the rows, the sort state and the page, and it draws them. Columns declare a header, a cell renderer, alignment, width, a monospace flag for references, a skeleton width for loading, and hideBelow to drop a column on narrow screens.

A sortable header is the whole cell, edge to edge: the button fills the th, so hover, press and focus light the full cell over the neutral sweep, never a pill inside it. The sort chevron always holds its slot, so labels never shift when sorting.

Every state is built in: loading draws skeleton rows matching the columns; empty renders whatever you pass (an EmptyState compact that says what to do next); error replaces the body (an Alert with a retry). Pagination sits centred under the table with a summary line.

Two appearances: glass (rows fall through to the frosted panel, the default inside a GlassPanel) and white (an inset solid table for dense lists). Both share one header look, set by --table-* tokens.

## Usage

```tsx
import { Pagination, Table, type Column, type SortState } from "@/components/ui";

const columns: Column<Invoice>[] = [
  { key: "ref", header: "Invoice", cell: (i) => i.reference, mono: true },
  { key: "customer", header: "Customer", sortable: true, cell: (i) => i.customer },
];

<Table caption="Invoices" columns={columns} rows={rows} rowKey={(i) => i.id} sort={sort} onSortChange={setSort} />
<Pagination page={page} pageCount={pageCount} onPageChange={setPage} totalItems={total} pageSize={20} itemLabel="invoices" />
```

## Props: Table

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` (required) | `ReadonlyArray<Column<T>>` |  |  |
| `rows` (required) | `ReadonlyArray<T>` |  |  |
| `rowKey` (required) | `(row: T) => string` |  |  |
| `caption` (required) | `string` |  | Screen-reader caption. Required: a table needs a name. |
| `sort` | `SortState` | `null` |  |
| `onSortChange` | `(sort: SortState) => void` |  | Header click: asc first, then toggles. Sort the rows yourself. |
| `loading` | `boolean` |  |  |
| `skeletonRows` | `number` | `5` |  |
| `empty` | `ReactNode` |  | Shown when not loading and rows is empty. Pass an <EmptyState compact />. |
| `error` | `ReactNode` |  | Replaces the body when the load failed. Pass an <Alert> with a retry. |
| `appearance` | `"glass" \| "white"` | `"glass"` | glass = rows fall through to the frosted panel (no wrapper background). white = the reference white inset table, for dense lists. |
| `isRowHighlighted` | `(row: T) => boolean` |  | Marks a row, e.g. the one just edited. |
| `className` | `string` |  |  |

## Props: Pagination

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `page` (required) | `number` |  |  |
| `pageCount` (required) | `number` |  |  |
| `onPageChange` (required) | `(page: number) => void` |  |  |
| `totalItems` | `number` |  |  |
| `pageSize` | `number` |  |  |
| `itemLabel` | `string` | `"results"` | Plural noun for the summary: "customers", "requests". |
| `className` | `string` |  |  |

## Examples

### Sortable, with pagination

```tsx
<Table caption="Invoices" columns={columns} rows={pageRows} rowKey={(i) => i.id} sort={sort} onSortChange={setSort} />
<Pagination page={page} pageCount={3} onPageChange={setPage} totalItems={11} pageSize={4} itemLabel="invoices" />
```

### Loading, empty and error

Switch the state to see each. Empty tells the user what to do next.

```tsx
<Table … loading />
<Table … rows={[]} empty={<EmptyState compact tone="filtered" title="No invoices match" description="Clear a filter to see more." />} />
<Table … error={<Alert tone="danger" title="Couldn't load invoices" action={<Button size="sm" variant="secondary" onClick={retry}>Try again</Button>} />} />
```

### White appearance

```tsx
<Table appearance="white" caption="Invoices" columns={columns} rows={rows} rowKey={(r) => r.id} />
```

## Theme tokens

Per-component tokens. Undeclared by default: the default below is the fallback the CSS uses. Set any of them at :root or on a wrapper to restyle every instance.

| Token | Default |
| --- | --- |
| `--button-primary-bg` | `var(--brand-gradient)` |
| `--button-radius` | `var(--r-sm)` |
| `--inner-radius` | `var(--r-lg)` |
| `--r-flat` | `var(--table-radius, var(--r-md))` / `var(--table-radius, var(--inner-radius, var(--r-lg)))` |
| `--table-radius` | `var(--r-md)` / `var(--inner-radius, var(--r-lg))` |
| `--table-row-height` | `46px` |

System tokens it reads: `--brand-deep`, `--brand-gradient`, `--control-h-sm`, `--dur`, `--ease`, `--focus-color`, `--focus-ring`, `--font-mono`, `--glass-fill-nested`, `--glow-sm`, `--ink-body`, `--ink-brand`, `--ink-disabled`, `--ink-inverse`, `--ink-muted`, `--line-glass`, `--line-soft`, `--r-lg`, `--r-md`, `--r-sm`, `--rgb-contact`, `--surface-solid`, `--table-cell-pad`, `--table-head-fill`, `--table-head-hover`, `--table-head-ink`, `--table-head-pad`, `--table-head-press`, `--table-head-rule`, `--table-row-hover`, `--table-row-line`, `--text-caption`, `--text-small`, `--wash-subtle`.

## Accessibility

- caption is required and rendered as a visually hidden <caption>: a table needs a name.
- Sortable headers are buttons inside the th with aria-sort on the th; the first press sorts ascending, then toggles.
- Rows are not clickable. Put a link (or a ghost Button with href) in the identifying cell, so keyboard and screen-reader users reach it.
- Pagination is a nav labelled "Pagination", the current page is aria-current="page", and prev/next disable at the ends.

## Do

- Put the table, its FilterBar and its Pagination in one GlassPanel: that is the list page pattern.
- Right-align numbers and actions (align="right").
- Give every empty state a next step.

## Don't

- Never invent another table look; theme --table-* tokens instead.
- Don't uppercase column headers; sentence case only.
- Don't put a title or an "n of m shown" line above the table.

Related: https://rime.mjsons.net/components/filter-bar, https://rime.mjsons.net/components/empty-state, https://rime.mjsons.net/components/glass-panel
