# DataTable

```tsx
import { DataTable } from '@tangible/ui';
```

Interactive docs and examples: [DataTable in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/components-datatable--docs).

## DataTable props

| Prop | Type | Default | Description |
| - | - | - | - |
| `columns` | `ColumnDef<T, unknown>[]` | | TanStack column definitions. The selection column is injected, not declared. |
| `data` | `T[]` | | Row data. Memoize it, or pass `autoResetPageIndex={false}` — see below. |
| `aria-label?` | `string` | | Defines a string value that labels the current element. |
| `aria-labelledby?` | `string` | | Identifies the element (or elements) that labels the current element. |
| `autoResetPageIndex?` | `boolean` | | TanStack's automatic page-0 reset when sorting or data identity change. Defaults to `true` when `pageIndex` is uncontrolled, `false` when controlled — a controlled parent owns the index, and the auto-reset would race it back to 0 on every re-render that recreates `data` (parent re-renders on page change → new array identity → reset). Uncontrolled consumers who `setState` inside `onPageIndexChange` while passing a derived (unmemoized) `data` array hit the same loop: pass `false` here or memoize `data`. |
| `className?` | `string` | | |
| `defaultSorting?` | `SortingState` | `[]` | Initial sort state. Sorting stays internal — this only seeds it. |
| `emptyMessage?` | `React.ReactNode` | `'No results'` | Shown in place of rows when `data` is empty. |
| `enableRowSelection?` | `boolean` | `false` | |
| `getRowId?` | `(row: T, index: number) => string \| number` | | |
| `getRowLabelForSelection?` | `(row: T, index: number) => string` | | |
| `getRowProps?` | `( row: TanRow<T> ) => Omit<React.ComponentProps<'tr'>, 'key' \| 'children' \| 'dangerouslySetInnerHTML'>` | | Extra props for each body `<tr>` — the escape hatch for row-level interaction: keyboard cursors, row click shortcuts, `data-*` hooks, context menus. Consumer `className` is merged after the built-in row classes; `data-selected` and the `is-selected` class always reflect actual selection state and cannot be overridden. `role`/`aria-*` are NOT guarded — overriding `role` on a `<tr>` removes it from the table's accessibility tree, so only do that as part of a deliberate pattern (e.g. rows in a `role="grid"` table keep `role="row"`). Loading/empty placeholder rows are not affected. |
| `initialPageSize?` | `number` | | |
| `labels?` | `DataTableLabels` | | Override hardcoded English strings for i18n. Unset keys use English defaults. |
| `loading?` | `boolean` | `false` | Replaces the body with a loading row while true. |
| `loadingMessage?` | `React.ReactNode` | `'Loading...'` | Shown in place of rows while `loading`. |
| `onPageIndexChange?` | `(pageIndex: number) => void` | | Fires with the requested 0-based page index whenever anything asks for a page change — pager clicks, page-size changes (TanStack re-anchors to keep the top row visible), the auto page-0 reset (see `autoResetPageIndex`), or an out-of-range controlled `pageIndex` being clamped into range. Fires in both controlled and uncontrolled modes. Note: 0-based, unlike Pager's 1-based `onPageChange` (the built-in pager converts). |
| `onRowSelectionChange?` | `(updater: Updater<RowSelectionState>) => void` | | |
| `onSelectionChange?` | `(selectedRows: T[], selectedIds: (string \| number)[]) => void` | | |
| `pageIndex?` | `number` | | Controlled page index (0-based, matching TanStack). When set, the parent owns paging: the built-in pager requests changes via `onPageIndexChange`, but the page only moves when this prop updates. Page size stays internal either way. Controlled/uncontrolled is locked at mount. |
| `pageSizeOptions?` | `number[]` | `[10, 20, 50]` | |
| `paginationMaxNumbers?` | `number` | `7` | |
| `paginationMode?` | `"ends" \| "full" \| "simple" \| "smart"` | `'smart'` | |
| `paginationNavStyle?` | `"icon" \| "text"` | `'text'` | |
| `renderBulkBar?` | `(args: BulkSelectionRenderArgs<T>) => React.ReactNode` | | |
| `renderPagination?` | `(table: TanTable<T>) => React.ReactNode` | | |
| `rowSelection?` | `RowSelectionState` | | |

Also accepts the standard attributes of `TableHTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

## Examples

- [Clickable Rows](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--clickable-rows): Whole-row click as a pointer shortcut via `getRowProps`. The row click must *duplicate* an accessible control (here the View button), never replace it — a `<tr>` with only an `onClick` is invisible to keyboard and AT. The handler also ignores clicks that land on the row's own interactive content.

- [Keyboard Cursor](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--keyboard-cursor): A consumer-owned keyboard cursor composed from `getRowProps` (roving `tabIndex`, `data-row-id`, accessible row names, Enter/Space activation on keydown) and controlled pagination (`pageIndex`/`onPageIndexChange`) — arrowing past the last row of a page flips to the next page and lands on its first row. The table is promoted to `role="grid"` (rest props spread onto the `<table>`), the APG signal that arrow keys belong to the app rather than the screen reader's browse cursor; the cursor ring uses the shared `--tui-focus-ring-*` tokens via `:focus-visible`, so it's keyboard-only and follows context remaps. Cursor movement reads DOM sibling order, not data order, so it stays correct under sorting; the row query relies on `.tui-table__row--body`, a stable class contract. The cursor deliberately does not touch `rowSelection`, which means "checked for bulk actions".

- [Controlled Pagination](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--controlled-pagination): When `pageIndex` is set the parent owns paging: the built-in pager requests changes through `onPageIndexChange` and the page moves only when the prop updates. Page size stays internal. `onPageIndexChange` also fires on the automatic reset to page 0 when sorting or data change.

- [Loading](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--loading): While `loading` is true the table sets `aria-busy` and shows `loadingMessage` in place of rows. Column headers stay mounted so the layout doesn't jump when data arrives.

- [Content Escaping](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--content-escaping): Cell values render as text, never as HTML — markup, template syntax, and entities in the data appear literally. Rich cell content goes through column `cell` renderers as React elements, not strings.

- [Localised Labels](https://storybook-tangible-ui.pages.dev/?path=/story/components-datatable--localised-labels): Every announced string routes through `labels`: selection checkboxes, sort buttons, and the live-region announcements for page changes and sorting. The scrollable container is a named `role="region"` (via `aria-label`/`aria-labelledby`) and is keyboard-focusable so keyboard users can scroll wide tables.

Other stories: Basic, Empty, With Actions, With Bulk Select ([interactive page](https://storybook-tangible-ui.pages.dev/?path=/docs/components-datatable--docs)).

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-table-border`
- `--tui-table-bg`
- `--tui-table-bg-head`
- `--tui-table-fg`
- `--tui-table-padding`
- `--tui-table-selected-bg`
- `--tui-table-selected-fg`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/components-datatable--docs
