Skip to content

DataTable

<caos-data-table>

the TABLE body of a list surface — a row per record, ordered by its column headings

May be placed on a record page, an app’s home page and a page inside an app.

Name Attribute Description Type Default Required
columns property only The columns, in order: each one a cell key, a header label, and optionally sortable, an alignment (numeric columns read end) and a fixed width. Set as a property — an attribute cannot hold a list. json none declared Required
rows property only The records. Each row carries an id, its cells keyed by column key, and optionally selected, disabled or total. A row may also carry children — the rows beneath it — which draw as ordinary rows at their own depth, with the same cells, checkbox and actions as any other. Nothing is fetched here: the access-filtered, formatted set is assembled by the app layer and handed in. An empty list is a real state and draws the empty content rather than an empty frame. json empty list Optional
sort property only Which column is sorted and which way, as { key, direction }. Setting it moves the header marker and aria-sort; it does NOT reorder the rows. The host reorders and hands back a new rows list. json null Optional
selectable selectable, also a property Add the checkbox column and the tri-state select-all in the header. A row flagged total is never selectable and gets no checkbox. boolean false Optional
expandable expandable, also a property Lets a reader open and close the rows under a parent row. Every parent still starts OPEN, so nothing a table was given is hidden on arrival. Without this, children still draw at their depth and simply cannot be put away. This is a MODE of this table, not a second hierarchical table: a nested row is an ordinary row, so selection and row actions reach it unchanged. It is the Tree input collapsible under the name a table calls it. boolean false Optional
loading loading, markup only The rows have not arrived. Draws placeholder rows in the real column shape instead of the body, so the table does not jump when the data lands. boolean false Optional
loadingRows loading-rows, markup only How many placeholder rows to draw while loading. number 3 Optional
emptyLabel empty-label, markup only The heading of the built-in empty content shown when there are no rows. text No records Optional
label label, markup only The accessible name of the grid — what a screen reader calls this table. Follows the attribute whenever it changes; without it the grid has no name. text none declared Optional
Name When it fires What it carries
caos-sort A sortable column header is pressed. The header marker moves immediately; the rows do not. { column, direction } — the column key, and asc or desc.
caos-row-select One row’s checkbox is ticked or cleared. { id, selected, selectedIds } — the row, its new state, and every selected row id.
caos-select-all The header checkbox is ticked or cleared. Disabled rows and total rows are left alone. { selected, selectedIds } — the new state, and every selected row id afterwards. Rows inside a closed parent are included: a reader who ticks select-all means every row, not every row they happen to be looking at.
caos-row-expand A parent row is opened or closed. Only an expandable table raises it. { id, expanded } — the parent row, and whether its children are now showing.
Name What goes in it
footer The strip under the table, kept outside the horizontal scroller so it stays put while a wide table scrolls. The source describes it as carrying the pagination affordance — and the library has no pagination piece, so there is nothing to put in it yet. Whatever is placed here is laid out as a row with its ends pushed apart, above a top rule.
empty What to show instead of rows when the list is empty. Left alone it holds a default EmptyState headed by empty-label; filling it replaces that, which is how a list says something more useful than “no records”.
(named by a cell) A cell whose value is the descriptor { slot: “name” } draws a slot of that name in place, so a real piece — a StatusPill in a stage column, a Tag, a ProvenanceMarker — is projected into the cell from the light DOM. The names are chosen by the data rather than by this component, so they cannot be listed here.
Design token What it controls
--caos-color-border-strong The border around the table card.
--caos-radius-lg The corners of the table card.
--caos-shadow-card The elevation of the table card.
--caos-color-surface The ground the table sits on.
--caos-color-surface-2 The hover tint of a row.
--caos-font The typeface of the cells.
--caos-text-row The text size of a row.
--caos-color-text The cell ink.
--caos-font-mono The header face, and the figures in an end-aligned column.
--caos-font-weight-label The weight of the column headers.
--caos-color-text-ghost The header ink at rest.
--caos-color-border The rule under the header and between the rows.
--caos-color-accent The rule above a total row, the links inside cells, and the selected-row tint.
--caos-color-accent-strong The header ink of the column currently sorted.
--caos-color-text-muted The em dash standing in for an empty cell.
--caos-font-display The figure on a total row.
--caos-focus-ring The focus ring on a sort header and on a cell link.
--caos-radius-sm The corners of that focus ring.
--caos-space-1 The gap between a header label and its sort arrow.
--caos-space-2 The gap between things inside a cell, and the checkbox column inset.
--caos-space-3 The footer padding down, and the gap between footer items.
--caos-space-4 The footer padding across.

Nothing else drives this piece by calling it.

Name Type What it tells you
selectedIds string[] The ids of the rows currently ticked. Selection is driven by the checkboxes and reported by the events; nothing sets it.
Part Which piece of it When it is there
::part(scroll) the box that scrolls when the table is wider than its space Always
::part(table) the table element Always
::part(head) the header section Always
::part(header-row) the row of column headers Always
::part(header-cell) one column header, the selection column included Always
::part(sort-button) the control inside a sortable column header Always
::part(select-all) the checkbox in the header of the selection column Always
::part(body) the section holding the rows Always
::part(row) one row Always
::part(cell) one cell, the selection cell included Always
::part(row-select) the checkbox in a row selection cell Always
::part(footer) the strip under the table holding slotted footer content Always

Several records that are being COMPARED, where the same field across records is the thing being read and the columns line those values up. It is also the only one of the four list bodies that can select rows or carry a totals line.

  • CardGrid — The records are being browsed rather than compared: a name, a sub-line and a couple of rollups each, in a grid that reflows on a narrow screen instead of scrolling sideways.
  • KanbanBoard — The question is WHERE THE WORK IS rather than what is in the list — the same records stood up in columns by the field that says what stage each one is at.
  • SplitView — Somebody works down the list reading one record at a time, and reading one should not cost a trip to its page and back.

The table is a real role=“grid”: each row is a row, each cell a gridcell, a sortable header is a real button carrying aria-sort, a selected row carries aria-selected, and the select-all checkbox goes indeterminate when only some rows are ticked. Focus roams in two dimensions — Arrow Up/Down between rows, Left/Right within a row, Home/End to the ends — and the stops are enumerated through the shadow-piercing focus util, so a control living inside a slotted piece’s own shadow root is reached in order. Left to the author: set label, because a grid with no accessible name is announced as a grid and nothing else; give any piece slotted into a cell its own name; and actually perform the sort when caos-sort fires, because the header marker says the list is sorted whether or not anybody reordered it.

Three records as rows and columns: two sortable headers, a status column holding real status pills, and the figures aligned to the right. The same three records the other three list bodies draw.

{
"id": "example",
"section": "The orders list",
"columns": 1,
"items": [
{
"id": "data_table_1",
"type": "component",
"key": "data_table",
"inputs": {
"label": "Orders",
"columns": [
{
"key": "reference",
"label": "Order",
"sortable": true
},
{
"key": "account",
"label": "Account"
},
{
"key": "status",
"label": "Status"
},
{
"key": "total",
"label": "Total",
"align": "end",
"sortable": true
}
],
"rows": [
{
"id": "o1",
"cells": {
"reference": "ORD-4471",
"account": "Meridian Supply",
"status": {
"slot": "status-o1"
},
"total": "12,400"
}
},
{
"id": "o2",
"cells": {
"reference": "ORD-4472",
"account": "Calder & Finch",
"status": {
"slot": "status-o2"
},
"total": "3,150"
}
},
{
"id": "o3",
"cells": {
"reference": "ORD-4473",
"account": "Brightwater Group",
"status": {
"slot": "status-o3"
},
"total": "86,900"
}
}
]
},
"children": [
{
"id": "status_pill_2",
"type": "component",
"key": "status_pill",
"inputs": {
"intent": "muted"
},
"slot": "status-o1",
"children": [
"Draft"
]
},
{
"id": "status_pill_3",
"type": "component",
"key": "status_pill",
"inputs": {
"intent": "success"
},
"slot": "status-o2",
"children": [
"Approved"
]
},
{
"id": "status_pill_4",
"type": "component",
"key": "status_pill",
"inputs": {
"intent": "info"
},
"slot": "status-o3",
"children": [
"In review"
]
}
]
}
]
}
<caos-data-table id="dataTable1" label="Orders">
<caos-status-pill slot="status-o1" intent="muted">Draft</caos-status-pill>
<caos-status-pill slot="status-o2" intent="success">Approved</caos-status-pill>
<caos-status-pill slot="status-o3" intent="info">In review</caos-status-pill>
</caos-data-table>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const dataTable1 = document.getElementById('dataTable1');
dataTable1.columns = [
{
"key": "reference",
"label": "Order",
"sortable": true
},
{
"key": "account",
"label": "Account"
},
{
"key": "status",
"label": "Status"
},
{
"key": "total",
"label": "Total",
"align": "end",
"sortable": true
}
];
dataTable1.rows = [
{
"id": "o1",
"cells": {
"reference": "ORD-4471",
"account": "Meridian Supply",
"status": {
"slot": "status-o1"
},
"total": "12,400"
}
},
{
"id": "o2",
"cells": {
"reference": "ORD-4472",
"account": "Calder & Finch",
"status": {
"slot": "status-o2"
},
"total": "3,150"
}
},
{
"id": "o3",
"cells": {
"reference": "ORD-4473",
"account": "Brightwater Group",
"status": {
"slot": "status-o3"
},
"total": "86,900"
}
}
];

The same list with the checkbox column on, one row already ticked, and a summary row under it — which carries no checkbox and is left out of select-all.

{
"id": "example",
"section": "Selecting rows, and a total",
"columns": 1,
"items": [
{
"id": "data_table_1",
"type": "component",
"key": "data_table",
"inputs": {
"label": "Orders",
"selectable": true,
"columns": [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
],
"rows": [
{
"id": "o1",
"cells": {
"reference": "ORD-4471",
"account": "Meridian Supply",
"total": "12,400"
},
"selected": true
},
{
"id": "o2",
"cells": {
"reference": "ORD-4472",
"account": "Calder & Finch",
"total": "3,150"
}
},
{
"id": "o3",
"cells": {
"reference": "ORD-4473",
"account": "Brightwater Group",
"total": "86,900"
}
},
{
"id": "sum",
"cells": {
"reference": "Total",
"account": "",
"total": "102,450"
},
"total": true
}
]
}
}
]
}
<caos-data-table id="dataTable1" label="Orders" selectable></caos-data-table>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const dataTable1 = document.getElementById('dataTable1');
dataTable1.columns = [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
];
dataTable1.rows = [
{
"id": "o1",
"cells": {
"reference": "ORD-4471",
"account": "Meridian Supply",
"total": "12,400"
},
"selected": true
},
{
"id": "o2",
"cells": {
"reference": "ORD-4472",
"account": "Calder & Finch",
"total": "3,150"
}
},
{
"id": "o3",
"cells": {
"reference": "ORD-4473",
"account": "Brightwater Group",
"total": "86,900"
}
},
{
"id": "sum",
"cells": {
"reference": "Total",
"account": "",
"total": "102,450"
},
"total": true
}
];

The rows have not arrived: placeholder lines are drawn in the real column shape, so the table is the same size before and after the data lands.

{
"id": "example",
"section": "Waiting for the rows",
"columns": 1,
"items": [
{
"id": "data_table_1",
"type": "component",
"key": "data_table",
"inputs": {
"label": "Orders",
"loading": true,
"loading_rows": 4,
"columns": [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
]
}
}
]
}
<caos-data-table id="dataTable1" label="Orders" loading loading-rows="4"></caos-data-table>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const dataTable1 = document.getElementById('dataTable1');
dataTable1.columns = [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
];

No rows at all: the headers stay and the body holds an EmptyState headed by empty-label, rather than a blank strip under the column names.

{
"id": "example",
"section": "Nothing in the list",
"columns": 1,
"items": [
{
"id": "data_table_1",
"type": "component",
"key": "data_table",
"inputs": {
"label": "Orders",
"empty_label": "No orders yet",
"columns": [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
],
"rows": []
}
}
]
}
<caos-data-table id="dataTable1" label="Orders" empty-label="No orders yet"></caos-data-table>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const dataTable1 = document.getElementById('dataTable1');
dataTable1.columns = [
{
"key": "reference",
"label": "Order"
},
{
"key": "account",
"label": "Account"
},
{
"key": "total",
"label": "Total",
"align": "end"
}
];
dataTable1.rows = [];