<!-- Protocore Design System — Table -->
# Table

- **Category:** Data Display (`data-display`)
- **Slug:** `data-display/table`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Table } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/data-display/table

> The styled table primitive — thin compound wrappers over the native table elements that carry the console look.

## Table vs DataTable

**Table** is the low-level styled primitive: you own the markup, the data, and any interaction. Use it for small, static, fully-controlled grids — a config summary, a fixed comparison, a handful of rows you render yourself.

**DataTable** is the server-driven console workhorse built *on top of* these recipes: TanStack columns, manual sorting/pagination, row selection, roving-tabindex keyboard nav, and the five body states (loading / empty / error / data). Reach for DataTable the moment a list is paginated, sortable, selectable, or fetched from a server. If you find yourself re-implementing sort headers or a selection bar over Table, switch to DataTable.

For key→value record display (one entity's fields) use **DefinitionList**, not a two-column Table.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `scrollLabel` | `string` | no | `Table` | Accessible name for the horizontal-scroll region (so keyboard users can Tab to and scroll it). |
| `sticky` | `boolean` | no | `false` | Make the header row stick to the top of the scroll container while the body scrolls. |
| `style` | `CSSProperties` | no | — | — |
| `wrapperClassName` | `string` | no | — | Extra className on the outer horizontal-scroll wrapper (the ref points at the `<table>`). |

## Examples

### Basics

Compose `Table.Head`, `Table.Body`, `Table.Row`, `Table.HeaderCell`, and `Table.Cell`. They're thin wrappers over `<thead>`/`<tbody>`/`<tr>`/`<th>`/`<td>` that add the console styling: mono uppercase header labels on a sunken surface, hairline row rules, no zebra.

```tsx
import { Table, Badge } from "@protocore/pds";

const runs = [
  { id: "run_4820", pipeline: "ledger-settle", status: "success", tone: "success" },
  { id: "run_4819", pipeline: "fx-reconcile", status: "running", tone: "info" },
  { id: "run_4818", pipeline: "ledger-settle", status: "failed", tone: "danger" },
] as const;

// The compound primitive: Table.Head / Body / Row / HeaderCell / Cell over the
// native elements, carrying the console look.
export default function TableBasics() {
  return (
    <Table>
      <Table.Head>
        <Table.Row>
          <Table.HeaderCell>Run</Table.HeaderCell>
          <Table.HeaderCell>Pipeline</Table.HeaderCell>
          <Table.HeaderCell>Status</Table.HeaderCell>
        </Table.Row>
      </Table.Head>
      <Table.Body>
        {runs.map((r) => (
          <Table.Row key={r.id}>
            <Table.Cell>{r.id}</Table.Cell>
            <Table.Cell>{r.pipeline}</Table.Cell>
            <Table.Cell>
              <Badge tone={r.tone}>{r.status}</Badge>
            </Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}
```

### Numeric columns

Mark amount columns `numeric` on both the header cell and the body cells: they right-align and render in tabular mono so figures line up on the decimal. `Table.Caption` adds a mono micro-label above the grid.

```tsx
import { Table, MoneyAmount } from "@protocore/pds";

const rows = [
  { account: "acct_eu_00417", debits: 128_40, credits: 512_00, net: 383_60 },
  { account: "acct_eu_00418", debits: 9_900_00, credits: 4_120_00, net: -5_780_00 },
  { account: "acct_eu_00419", debits: 0, credits: 42_500_00, net: 42_500_00 },
];

// `numeric` right-aligns header + cell and renders values in tabular mono, so
// amount columns line up on the decimal.
export default function TableNumeric() {
  return (
    <Table>
      <Table.Caption>Daily net movement · EUR</Table.Caption>
      <Table.Head>
        <Table.Row>
          <Table.HeaderCell>Account</Table.HeaderCell>
          <Table.HeaderCell numeric>Debits</Table.HeaderCell>
          <Table.HeaderCell numeric>Credits</Table.HeaderCell>
          <Table.HeaderCell numeric>Net</Table.HeaderCell>
        </Table.Row>
      </Table.Head>
      <Table.Body>
        {rows.map((r) => (
          <Table.Row key={r.account}>
            <Table.Cell>{r.account}</Table.Cell>
            <Table.Cell numeric>
              <MoneyAmount amount={r.debits} minorUnits={2} currency="EUR" />
            </Table.Cell>
            <Table.Cell numeric>
              <MoneyAmount amount={r.credits} minorUnits={2} currency="EUR" />
            </Table.Cell>
            <Table.Cell numeric>
              <MoneyAmount amount={r.net} minorUnits={2} currency="EUR" sign="always" />
            </Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}
```

## Usage

**Do**

- Use Table for small, static, hand-rendered grids you fully control.
- Mark amount columns `numeric` on both header and body cells so they align.
- Set `sticky` when a tall table scrolls inside a fixed-height region.
- Graduate to DataTable as soon as you need sorting, pagination, or selection.

**Don't**

- Don't hand-roll sort headers or a selection bar over Table — that's DataTable's job.
- Don't use a two-column Table for one record's fields; use DefinitionList.
- Don't add zebra striping or box-shadows; the hairline console look is deliberate.
- Don't forget an `onKeyDown` when you make rows `interactive` — hover alone isn't keyboard-operable.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Moves through any focusable controls inside cells. Rows themselves are not focusable unless you make them so. |

**Notes**

- Table renders semantic `<table>`/`<thead>`/`<tbody>`/`<th scope="col">` markup, so screen readers announce row/column structure natively.
- `Table.Caption` becomes a real `<caption>` — the accessible name for the whole grid.
- `interactive`/`selected` are visual only. If a row is a click target, add `onKeyDown` (Enter/Space), a `tabIndex`, and an appropriate role — or use DataTable, which wires roving-tabindex navigation for you.
- The root wraps the table in an `overflow-x` scroll region so wide grids scroll instead of forcing the page to scroll horizontally.

## Related

`data-table`, `definition-list`, `badge`, `status-dot`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
