Table

Rows one square high, each standing on a hairline at its foot, with no box and no column lines. Column headers keep the text’s weight in the title grey; the first cell of each row is its row header. Numbers are set to the end of their column in tabular figures. Nothing is filled with the accent except a cell someone has selected.

Print stock

Text columns and numeric columns share rows; figures align at the end of their columns.

Reading room loans

Text columns and numeric columns share rows; figures align at the end of their columns.

A table with data

Rows and the totals are assigned in JavaScript; the headers come from the keys.

Set the properties after the component has been registered. In Lit, bind them: .data=${rows}.

PropertyTypeDefaultWhat it does
dataArray[]The body rows: objects read by column key, or arrays read by position.
columnsArray[]Headers. A string is a label read from its lowercase key ("Users" reads users); an object is { key, label, numeric? }. Empty, the first row’s keys are the headers.
footerObject or ArraynoneA totals row under the body.
stickybooleanfalseKeeps the first column in place while the table scrolls sideways.
sheetbooleanfalseA spreadsheet: every cell four squares by one, columns lettered and rows numbered. See Sheet.
reorderablebooleanfalseA sheet’s rows move by the grip on their number. See Moving rows.
labelstringnoneThe table’s accessible name.

A column whose values are all numbers is numeric unless its numeric says otherwise. A table wider than its place scrolls sideways.

const table = document.querySelector("ds-table");
table.columns = ["Product", { key: "users", label: "Users", numeric: true }, "Status"];
table.data = [{ product: "Atlas", users: 120, status: "Active" }];
table.footer = { product: "Total", users: 120 };

data-plain on a native .ds-table takes every line away: the same rows with no rule at their foot, standing on the paper as a List’s rows do, their words still on the lower sixteen of each square and the headers in the title grey. For a table whose rows are read as a list beside one (Money’s tables, Link’s clients).

<table class="ds-table" data-plain>…</table>

data-frame on a native .ds-table draws the same rows in a rounded 1px frame with a 1px line between columns, for a list of things a person opens. Every line is drawn inside the cells’ squares, so each row keeps its 20px pitch and the last row’s rule is the frame’s foot; words stand 4px in from the lines, and fills in the cells stay inside the corners (--ds-table-radius, the card’s corner by default).

<table class="ds-table" data-frame>…</table>

For a lot of data, as in a spreadsheet, give the table sheet. Every cell is four squares wide and one high, 80 by 20, with its 1px lines at its end and its foot, so its words have 79 by 19 and stand 4px in on the sixteen, as an Ezo item’s do. Numbers keep to the column’s end. A cell’s words never widen it: what does not fit fades out over the cell’s last square, never an ellipsis. The fade is on the words alone, so it fades into whatever is behind them, the paper or a selected or hovered cell’s fill, and only where they are cut: words that fit stay whole. Words fade at their end; a number keeps its last digits against the column’s end and fades its first ones, in either direction of writing. Above the column labels a row of letters (A, B … Z, AA …) names the columns, and a column two squares wide numbers the rows; both stay in place while the sheet scrolls both ways. A selected cell (aria-selected="true") is ringed rather than filled. With reorderable the rows move by a grip on their number; see Moving rows.

A year of print sales

A sheet: 26 lettered columns and 500 numbered rows, every cell four squares by one; the letters, labels and row numbers stay while it scrolls both ways. Drag a row by the grip on its number (hold it on a touch screen), or press Alt+↑ or Alt+↓ on the grip; the rows renumber.

const sheet = document.querySelector("ds-table[sheet]");
sheet.style.setProperty("--ds-table-height", "400px");
sheet.columns = ["Item", { key: "sold", label: "Sold", numeric: true }];
sheet.data = rows;

On a native table, put data-sheet and --ds-table-columns (its number of data columns) on the <table>, a .ds-table__letters row and a .ds-table__number cell at the start of every row, the words of each cell in a .ds-table__words span, and give .ds-table-scroll its height with --ds-table-height. Without measuring, every cell’s end fades; add data-fit to the table and mark the cut words yourself with data-cut="end" (or "start" for a number scrolled to its end) to fade only those, as <ds-table sheet> does.

<div class="ds-table-scroll" style="--ds-table-height: 400px">
<table class="ds-table" data-sheet style="--ds-table-columns: 2">
<thead>
<tr class="ds-table__letters" aria-hidden="true"><td class="ds-table__number"></td><td>A</td><td>B</td></tr>
<tr><td class="ds-table__number"></td><th scope="col"><span class="ds-table__words">Item</span></th><th scope="col" data-numeric><span class="ds-table__words">Sold</span></th></tr>
</thead>
<tbody><tr><th scope="row" class="ds-table__number">1</th><td><span class="ds-table__words">Atlas</span></td><td data-numeric><span class="ds-table__words">1,200</span></td></tr></tbody>
</table>
</div>

Give a sheet reorderable and its rows move, as items in a sortable list do. Hover a row and its number gives way to a grip, the handle icon. Press the grip and move 5px with a mouse, or hold it for 250ms with a finger (a finger that moves first scrolls the sheet), and the row lifts: it stays on the paper with a hairline above it and follows the pointer, while the rows it passes step aside by one row each, in transform alone, in 200ms on cubic-bezier(0.2, 0, 0, 1). Near the sheet’s top or foot it scrolls, faster the closer the pointer. Let go and the row lands on a whole row, in 330 to 550ms by its distance on cubic-bezier(.2, 1, .1, 1), and the numbers follow it. Escape puts it back. Under reduced motion the rows move at once.

The grips take the keyboard as one stop: ↑ and ↓, Home and End move between them, and Alt+↑ or Alt+↓ (or ⌘⇧↑ and ⌘⇧↓, Ctrl+Shift elsewhere, as a writing surface moves a block) moves the row one place, the focus going with it. Each grip is named “Move row 3”, and each move is announced, “Row 3 moved to 5”, in the page’s language with the shared words.

Every move asks first with a cancelable table-move whose detail is { from, to }, both counted from 0. Left alone, the table moves the row in its own data (a new array; read it back from table.data). When the rows are the page’s, prevent it, move them in your own state and set data again; the row waits where it was dropped until they arrive, and glides back if they do not. moveRow(rows, from, to) from ds-table.ts gives the new order.

import { moveRow } from "@0001-labs/design-system-1/DS1/3-unit/ds-table";
sheet.reorderable = true;
sheet.addEventListener("table-move", event => {
event.preventDefault();
store.rows = moveRow(store.rows, event.detail.from, event.detail.to);
sheet.data = store.rows;
});

In React, Table takes onMove, and with it the rows are the page’s:

import { Table } from "@0001-labs/design-system-1/DS1/react/table";
import { moveRow } from "@0001-labs/design-system-1/DS1/3-unit/ds-table";
const [rows, setRows] = useState(sales);
<Table sheet reorderable label="A year of print sales" columns={columns} data={rows}
onMove={({ from, to }) => setRows(current => moveRow(current, from, to))} />

The totals row stays where it is. Moving rows belongs to the element; a native .ds-table[data-sheet] has no grips.

Row · Use with HTML

On a native .ds-table, mark the body row whose details are open with data-open. Its cells keep the shared current-row fill, including the sticky first cell, while selected cells keep their selection colour. Remove the attribute on close or move it when navigating between records. Keep aria-expanded on the row’s detail button in step. This is a controlled state; it does not add click or keyboard behavior to a table row.