---
title: Table
description: Table provides a composable data grid with keyboard navigation, row selection, and sortable columns.
---

import { meta as TableMeta, Default, MultipleSelection, Sorting } from '@/content/stories/table';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';

export const config = {
	dependencies: {
		components: [Canvas, Demo],
		scripts: [
			'../../../components/component-docs/demo.script.tsx',
			'../../../components/component-docs/canvas.script.tsx',
			'../../../components/component-docs/controls.script.tsx',
		],
	},
};


# Table

<p class="docs-lede">Table provides a composable data grid with keyboard navigation, row selection, and sortable columns.</p>

## Try it

<Demo of={Default} meta={TableMeta} />

## Usage

Compose header, body, columns, and rows as light-DOM children. The table owns keyboard focus, selection, and sort state; your app owns the collection and updates it after `rui-sort-change`. Selected row ids live on `value` as `string[]`; the HTML attribute is comma-separated. JSX may pass a string or an array. `rui-change` emits `{ value: string[] }`.

```tsx
import {
  RuiTable,
  RuiTableBody,
  RuiTableCell,
  RuiTableColumn,
  RuiTableHeader,
  RuiTableRow,
} from '@ecopages/radiant-ui/table';

<RuiTable label="Plants" sortColumn="name" sortDirection="ascending">
  <RuiTableHeader>
    <RuiTableColumn id="name" allowsSorting isRowHeader>
      Plant
    </RuiTableColumn>
    <RuiTableColumn id="cycle" allowsSorting>
      Cycle
    </RuiTableColumn>
  </RuiTableHeader>
  <RuiTableBody>
    <RuiTableRow id="aloe">
      <RuiTableCell isRowHeader>Aloe</RuiTableCell>
      <RuiTableCell>Perennial</RuiTableCell>
    </RuiTableRow>
  </RuiTableBody>
</RuiTable>
```

## Custom markup

`<rui-table>` coordinates any light-DOM tree that matches its query contract. The
`Rui*` helpers stamp these targets; they are not required.

```tsx
import '@ecopages/radiant-ui/table';

<rui-table label="Plants" sort-column="name" sort-direction="ascending">
  <div class="rui-table" role="grid" aria-label="Plants">
    <div role="rowgroup" class="rui-table__header">
      <div role="row" class="rui-table__row">
        <div role="columnheader" data-table-column="name">
          <button type="button" data-table-sort="name">Plant</button>
        </div>
        <div role="columnheader" data-table-column="cycle">Cycle</div>
      </div>
    </div>
    <div role="rowgroup" class="rui-table__body">
      <div role="row" data-table-row="aloe" class="rui-table__row">
        <div role="rowheader" data-table-cell class="rui-table__cell" tabindex={-1}>Aloe</div>
        <div role="gridcell" data-table-cell class="rui-table__cell" tabindex={-1}>Perennial</div>
      </div>
    </div>
  </div>
</rui-table>;
```

Add `data-table-actionable` on a row for Enter / double-click activation. Use `rui-checkbox[data-table-select-row]` for per-row checkboxes.

## Row selection

Set `selectionMode="multiple"` (or `"single"`) and compose `RuiTableSelectionCell` in the header and each row. Click-to-select is not a separate prop: when selection is enabled, clicking a data cell selects that row. Nested controls (`button`, `a`, `input`, `rui-checkbox`) do not select. Space toggles selection. Enter and double-click fire `rui-row-action` on rows with `data-table-actionable`.

<Canvas of={MultipleSelection} meta={TableMeta} />

## Sorting

`allowsSorting` on `RuiTableColumn` adds the sort button and indicator. Clicking it updates `sort-column`, `sort-direction`, and `aria-sort`, then emits `rui-sort-change`. The table does not reorder rows. Listen for the event and render the collection in that order:

```tsx
table.addEventListener('rui-sort-change', (event) => {
  const { column, direction } = event.detail;
  plants = sortPlants(plants, column, direction);
});
```

<Canvas of={Sorting} meta={TableMeta} />

## Table vs grid vs treegrid

<p>Use `RuiGrid` for simple read-only grids with a `rows` prop. Use `RuiTreegrid` when rows expand into hierarchies. Use `RuiTable` when you need sortable columns, row selection, and actionable row activation.</p>

## Theming

| Part | CSS roles |
| --- | --- |
| Surface (`rui-table`) | `border`, `background`, `on-background`, `rounded-container`, `shadow-sm` |
| Header columns | `surface`, `on-background` |
| Selected row | `primary-container`, `on-primary-container` |
| Sort / focus | `focus-ring` |

## API

`RuiTable` is a custom element (`<rui-table>`) with composable JSX helpers for header, body, columns, cells, and selection.

### Attributes (`<rui-table>`)

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `''` | Accessible name for the grid. |
| `aria-busy` | `'true'` · `'false'` | `'false'` | Whether the collection is updating. |
| `selection-mode` | `none` · `single` · `multiple` | `none` | Row selection mode. `single` and `multiple` select a row when a non-interactive cell is clicked. |
| `value` | `string` | omitted | Comma-separated selected row ids. Empty selection removes the attribute. The JS property is `string[]`. |
| `sort-column` | `string` | `''` | Active sortable column id. |
| `sort-direction` | `ascending` · `descending` | `ascending` | Active sort direction. |

### Light-DOM contract

| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-table-row]` | yes | `aria-selected` when selecting | `data-table-row` id, `data-table-actionable`, `hidden`, `aria-disabled` |
| `[data-table-cell]` | yes | roving `tabIndex` | cell content; direct child of a row |
| `[data-table-column]` | per header | `aria-sort` when active | column id |
| `[data-table-sort]` | per sortable column | — | sort button |
| `rui-checkbox[data-table-select-row]` | no | `checked`, `disabled` | nested row checkbox |
| `rui-checkbox[data-table-select-all]` | no | `checked`, `indeterminate`, `disabled` | nested header checkbox |

Do not set `aria-selected`, `aria-sort`, or checkbox `checked` — the host owns those.

Nested hosts: `rui-checkbox` with `data-table-select-row` or `data-table-select-all`.

### Events

| Event | Detail | Description |
| --- | --- | --- |
| `rui-change` | `{ value: string[] }` | Selected row ids changed. |
| `rui-sort-change` | `{ column, direction }` | Sort column or direction changed. The table does not reorder rows. |
| `rui-row-action` | `{ rowId: string }` | Actionable row activated (Enter / double-click). |

### View helpers

| Component | Target stamped | Notes |
| --- | --- | --- |
| `RuiTable` | `.rui-table[role="grid"]` wrapper | Not queried; hosts `label` / `aria-busy` on the grid. |
| `RuiTableColumn` | `[data-table-column]`, optional `[data-table-sort]` | `allowsSorting` adds the sort button. |
| `RuiTableRow` | `[data-table-row]`, optional `data-table-actionable` | Row id via `id` prop. |
| `RuiTableCell` | `[data-table-cell]` | `isRowHeader` sets `role="rowheader"`. |
| `RuiTableSelectionCell` | `[data-table-cell]`, `rui-checkbox[data-table-select-*]` | `scope="all"` or `"row"`. |

### CSS classes

| Class | Description |
| --- | --- |
| `.rui-table` | Table surface (`role="grid"`). |
| `.rui-table__column` | Column header cell. |
| `.rui-table__cell` | Data cell. |
| `.rui-table__row` | Data row. |
