---
title: Table
description: Table provides a composable data grid with keyboard navigation, row selection, and sortable columns.
category: Data display
---
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
Table provides a composable data grid with keyboard navigation, row selection, and sortable columns.
## Try it
## 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';
Plant
Cycle
Aloe
Perennial
```
## Custom markup
`` 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';
;
```
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`.
## 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);
});
```
## Table vs grid vs treegrid
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.
## 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 (``) with composable JSX helpers for header, body, columns, cells, and selection.
### Attributes (``)
| 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. |