# Table

> The composable table root: a scroll wrapper around a semantic table with density, striping, and a sticky header. Compose with the sibling TableRow, TableHeaderCell, and TableCell components. The presentational layer beneath DataTable; pair with the useTable* hooks for behavior.

- Status: stable
- Group: data-display
- Page: https://www.digitaltableteur.com/design-system/components/table
- Contract: https://www.digitaltableteur.com/contracts/v1/Table.contract.json

## When to use

Table renders semantic tabular data: a caption-led <table> whose rows and cells come from TableRow, TableHeaderCell and TableCell, with sorting and selection driven by the useTable* hooks rather than cell-local state. Use it when the content really is a data grid; DataTable wraps it for the batteries-included workflow.

## When not to use

- Do not use it for page layout.
- Do not leave the table without an accessible name; pass a caption, or aria-labelledby or aria-label when a visible heading already names it.
- Do not put interactive sort logic in the cell instead of the useTable* hooks.

## Look-alikes

- ProjectList: Do not use for tabular data the reader sorts or compares; that is a Table.
- StatList: Do not use it for tables of several values per row; use Table.

## Props

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `caption` | `string` | none | Accessible table name, rendered as a `<caption>`. Optional only when the table is named by aria-labelledby or aria-label. | No |
| `hideCaption` | `boolean` | `false` | Visually hide the caption while keeping the accessible name. Defaults to `false`. | No |
| `size` | `"sm" \| "md" \| "lg"` | `md` | Density scale. Defaults to `md`. | No |
| `striped` | `boolean` | `false` | Alternating row surfaces. Defaults to `false`. | No |
| `stickyHeader` | `boolean` | `false` | Pin the header row while the table body scrolls. Defaults to `false`. | No |
| `wrapperClassName` | `string` | none | Class applied to the scroll wrapper. | No |
| `stackOnNarrow` | `boolean` | `false` | Below a 40rem container width, stack each body row as a block instead of squeezing columns, with every cell prefixed by its TableCell label. Table sets role=table and the row and cell components add their own roles, so assistive technology keeps table semantics; give thead and tbody role="rowgroup" yourself. Defaults to `false`. | No |

## Variants

- Available sizes: `sm`, `md`, `lg` (default `md`).
- Allowed `striped` values: `true`, `false` (default `false`).
- Allowed `stickyHeader` values: `true`, `false` (default `false`).
- Allowed `stackOnNarrow` values: `true`, `false` (default `false`).

## Invalid combinations

- MUST NOT: When `caption` is not set, then set one of `aria-labelledby`, `aria-label`. A table without a caption has no accessible name. Pass caption, or aria-labelledby pointing at a visible heading, e.g. <Table aria-labelledby="usage-heading">.

## Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to the control and on to the next focusable element. (standard) |
| Space | Activates or toggles the focused control. (standard) |
| Enter | Activates the focused control. (standard) |

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

## Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

| Level | Rule | How it is checked |
| --- | --- | --- |
| MUST NOT | When `caption` is not set, then set one of `aria-labelledby`, `aria-label`. A table without a caption has no accessible name. Pass caption, or aria-labelledby pointing at a visible heading, e.g. <Table aria-labelledby="usage-heading">. | Checked by the MCP tool validate_component_usage (rule `table-needs-name`). Also enforced in code: Table.tsx development warning (!caption && !aria-labelledby && !aria-label). |
| MUST | ARIA: Native table, thead, tbody, th and td semantics. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Caption, aria-labelledby or aria-label supplies the accessible table name (a dev warning fires when none is given) | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: When the table overflows its wrapper, the wrapper becomes a focusable (tabIndex 0) role=region named like the table, so keyboard users can scroll it. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Sortable headers expose aria-sort and toggle via an inner button. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Sort caret is decorative (aria-hidden); state is carried by aria-sort. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by `npm run test:stories:matrix:ci`. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by `npm run test:stories:matrix:ci`. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by `npm run test:stories:hc`. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |

## Install and import

- Install: `npm install @digitaltableteur/react` (version 0.9.1, restricted access on npm).
- Import: `import { Table } from "@digitaltableteur/react";`
- Entry point: `@digitaltableteur/react/content` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once at the app boundary.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Table.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/table.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.
