Data table
A table of rows with sortable columns and row selection.
import '@usezeno/tokens/css';
import '@usezeno/core/web-components';
Examples
Basic, with sorting
Set columns and data as properties. Each row needs an id. A column sorts on click when it has sortable: true. A formatter changes the text of a cell; sorting still uses the raw value.
<zeno-data-table></zeno-data-table>
const table = document.querySelector('zeno-data-table');
table.columns = [
{ key: 'name', label: 'File', sortable: true },
{ key: 'owner', label: 'Owner', sortable: true },
{ key: 'size', label: 'Size', sortable: true, align: 'right', formatter: value => value + ' KB' },
];
table.data = [
{ id: 'f1', name: 'report.pdf', owner: 'Ada', size: 482 },
{ id: 'f2', name: 'notes.txt', owner: 'Grace', size: 3 },
{ id: 'f3', name: 'budget.csv', owner: 'Alan', size: 57 },
{ id: 'f4', name: 'plan.docx', owner: 'Ada', size: 120 },
];
Selectable rows
selectable adds a checkbox to each row and one to the header. row-select and select-all carry the ids of the selected rows. striped shades every second row.
No rows selected.
<zeno-data-table selectable striped></zeno-data-table>
<p data-result>No rows selected.</p>
const table = document.querySelector('zeno-data-table');
const result = document.querySelector('[data-result]');
table.columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
];
table.data = [
];
const show = event => {
const ids = event.detail.selectedRows;
result.textContent = ids.length ? 'Selected: ' + ids.join(', ') : 'No rows selected.';
};
table.addEventListener('row-select', show);
table.addEventListener('select-all', show);
Sizes
size changes the padding and the text size. The default is md.
<zeno-data-table size="sm"></zeno-data-table>
<zeno-data-table size="lg"></zeno-data-table>
document.querySelectorAll('zeno-data-table').forEach(table => {
table.columns = [
{ key: 'task', label: 'Task' },
{ key: 'status', label: 'Status' },
];
table.data = [
{ id: 't1', task: 'Write the summary', status: 'Done' },
{ id: 't2', task: 'Check the numbers', status: 'In progress' },
];
});
Loading and empty
loading fades the table and shows a spinner. The wrapper with position: relative keeps the spinner over the table; see the gaps below. A table with no rows shows emptyMessage.
<div style="position: relative">
<zeno-data-table loading></zeno-data-table>
</div>
<zeno-data-table></zeno-data-table>
const tables = document.querySelectorAll('zeno-data-table');
const columns = [
{ key: 'name', label: 'File' },
{ key: 'changed', label: 'Last changed' },
];
tables[0].columns = columns;
tables[0].data = [
{ id: 'f1', name: 'report.pdf', changed: 'Today' },
{ id: 'f2', name: 'notes.txt', changed: 'Yesterday' },
];
tables[1].columns = columns;
tables[1].data = [];
tables[1].emptyMessage = 'No files yet.';
Pages
show-pagination splits the rows into pages of page-size rows and shows a zeno-pagination under the table.
<zeno-data-table show-pagination page-size="5"></zeno-data-table>
const table = document.querySelector('zeno-data-table');
table.columns = [
{ key: 'number', label: 'Invoice', sortable: true },
{ key: 'amount', label: 'Amount', sortable: true, align: 'right' },
];
table.data = Array.from({ length: 12 }, (unused, index) => ({
id: 'invoice-' + (index + 1),
number: 'Invoice ' + (index + 1),
amount: 100 + index * 25,
}));
Attributes and properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
columns | columns | DataTableColumn[] | [] | |
data | data | DataTableRow[] | [] | |
size | sizereflects | 'sm' | 'md' | 'lg' | 'md' | |
striped | stripedreflects | boolean | false | |
hoverable | hoverablereflects | boolean | true | |
responsive | responsivereflects | boolean | true | |
stickyHeader | sticky-headerreflects | boolean | false | |
sortable | sortablereflects | boolean | true | |
selectable | selectablereflects | boolean | false | |
loading | loadingreflects | boolean | false | |
emptyMessage | emptymessage | string | 'No data available' | |
pageSize | page-size | number | 10 | |
showPagination | show-pagination | boolean | false | |
testId | string | Test ID for automated testing From ZenoElement. |
Methods
| Method | Description |
|---|---|
focus(): void | Focus the component or its first focusable element From ZenoElement. |
blur(): void | Blur the component or its focused element From ZenoElement. |
Events
| Event | Detail | Description |
|---|---|---|
sort | { column: string | undefined; direction: ('asc' | 'desc' | null) } | A sortable column header was activated. |
row-select | { row: DataTableRow; selected: boolean; selectedRows: Array<string | number> } | A row was selected or deselected. |
select-all | { selected: boolean; selectedRows: Array<string | number> } | The select-all checkbox changed. |
row-click | { row: DataTableRow } | A row was clicked. |
Slots
This element has no slots.
CSS parts
| Part |
|---|
body |
cell |
empty |
header |
header-cell |
header-row |
loading |
pagination |
row |
table |
wrapper |
CSS custom properties
This element documents no custom properties of its own. Style it through its parts and through the tokens it reads.
The element's own source reads no tokens.
Types
interface DataTableColumn {
key: string;
label: string;
sortable?: boolean;
width?: string;
align?: 'left' | 'center' | 'right';
formatter?: (_value: unknown, _row: DataTableRow) => string;
}
interface DataTableRow {
id: string;
[key: string]: unknown;
}
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus through the column headers, the row checkboxes of a selectable table and the page buttons. |
| Space | On a row checkbox, selects or deselects the row. |
Forms
This element is not a form control. It adds nothing to a form's data.
Notes
- Renders a native
<table>with<thead>,<tbody>,<th>and<td>. - Row selection uses
zeno-checkbox. The header checkbox is named “Select all rows” and shows a mixed state when only some rows of the page are selected. - The sorted column header gets
aria-sort. - While
loadingis set, thezeno-spinnerover the table is a polite live region named “Loading”. - On screens up to 768px wide, a
responsivetable hides the header row and shows each row as a block. The column label is printed before each value by CSS.
Tests and known gaps
- No tests of its own. The only check is that the element registers and renders (
smoke.test.ts). - Not checked with axe-core.
- Has no Storybook story.
24 of the 31 properties, methods, events, slots and parts above have no description in the source
- the property
columns - the property
data - the property
size - the property
striped - the property
hoverable - the property
responsive - the property
stickyHeader - the property
sortable - the property
selectable - the property
loading - the property
emptyMessage - the property
pageSize - the property
showPagination - the part
body - the part
cell - the part
empty - the part
header - the part
header-cell - the part
header-row - the part
loading - the part
pagination - the part
row - the part
table - the part
wrapper
Known gaps:
- Sorting does not work with the keyboard. A sortable header can take focus, but Enter and Space do nothing, because the header only listens for clicks.
- A sortable header has
role="button"on the<th>. That replaces the column header role of the cell, andaria-sortis not allowed on a button. aria-sortis set toascordesc. The valid values areascendinganddescending.- A column without
sortablelooks sortable and does nothing. The header gets the sort icon, the pointer cursor and a tab stop unlesssortableisfalse, but a click sorts only whensortableistrue. hoverable,responsiveandsortableare on by default, and a Boolean attribute cannot be set to false in HTML. They can only be turned off from JavaScript, for exampletable.sortable = false.- Every row shows a pointer cursor and fires
row-click, but rows cannot take focus. A row action is not possible with the keyboard. - The table has no accessible name. There is no caption and no label attribute or slot for one.
- Every row checkbox has the same name, “Select row”. A screen reader user cannot tell which row a checkbox belongs to.
- The texts “Select all rows”, “Select row” and “No data available” are English. Only the last one can be changed, with
emptyMessage. - The selection can only be read from the detail of
row-selectandselect-all. There is no property to read it, set it or clear it. - With pages, the header checkbox selects only the rows of the page that shows.
- The selection and the page number are kept when
datachanges. Ids of rows that are gone stay inselectedRows. When the new list is shorter, the table can stay on a page that no longer exists: it showsemptyMessage, and with only one page left there are no page buttons to go back. - A row with no
id, or with the id0or an empty string, is tracked by its position indata. Its selection moves to another row when the order ofdatachanges. - A cell holds text only. A
formatterreturns a string and there is no slot for a cell, so a cell cannot hold a link, a button or a badge. - A column can declare
width, but the table never uses it. - The loading spinner is not kept inside the table. It is centred in the nearest positioned ancestor, which can be the whole page. Until this is fixed, put the table in an element with
position: relative. loadingblocks the mouse only. It does not setaria-busyon the table, and the headers and checkboxes under the spinner can still be reached with Tab and used.- There is no property for the first sort, and no way to go back to the unsorted order once a column is sorted.
- The attribute for
emptyMessageisemptymessage, with no hyphen, whilepage-sizeandshow-paginationon the same element have one.