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[] }.
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.
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.
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:
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 (<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. |