Skip to main content

Data table

<zeno-data-table>Data displaySource

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 = [
{ id: 'ada', name: 'Ada', email: 'ada@example.com', role: 'Owner' },
{ id: 'grace', name: 'Grace', email: 'grace@example.com', role: 'Editor' },
{ id: 'alan', name: 'Alan', email: 'alan@example.com', role: 'Viewer' },
];
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​

PropertyAttributeTypeDefaultDescription
columnscolumnsDataTableColumn[][]
datadataDataTableRow[][]
sizesizereflects'sm' | 'md' | 'lg''md'
stripedstripedreflectsbooleanfalse
hoverablehoverablereflectsbooleantrue
responsiveresponsivereflectsbooleantrue
stickyHeadersticky-headerreflectsbooleanfalse
sortablesortablereflectsbooleantrue
selectableselectablereflectsbooleanfalse
loadingloadingreflectsbooleanfalse
emptyMessageemptymessagestring'No data available'
pageSizepage-sizenumber10
showPaginationshow-paginationbooleanfalse
testIdstringTest ID for automated testing From ZenoElement.

Methods​

MethodDescription
focus(): voidFocus the component or its first focusable element From ZenoElement.
blur(): voidBlur the component or its focused element From ZenoElement.

Events​

EventDetailDescription
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​

KeyWhat it does
TabMoves focus through the column headers, the row checkboxes of a selectable table and the page buttons.
SpaceOn 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 loading is set, the zeno-spinner over the table is a polite live region named “Loading”.
  • On screens up to 768px wide, a responsive table 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, and aria-sort is not allowed on a button.
  • aria-sort is set to asc or desc. The valid values are ascending and descending.
  • A column without sortable looks sortable and does nothing. The header gets the sort icon, the pointer cursor and a tab stop unless sortable is false, but a click sorts only when sortable is true.
  • hoverable, responsive and sortable are on by default, and a Boolean attribute cannot be set to false in HTML. They can only be turned off from JavaScript, for example table.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-select and select-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 data changes. Ids of rows that are gone stay in selectedRows. When the new list is shorter, the table can stay on a page that no longer exists: it shows emptyMessage, and with only one page left there are no page buttons to go back.
  • A row with no id, or with the id 0 or an empty string, is tracked by its position in data. Its selection moves to another row when the order of data changes.
  • A cell holds text only. A formatter returns 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.
  • loading blocks the mouse only. It does not set aria-busy on 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 emptyMessage is emptymessage, with no hyphen, while page-size and show-pagination on the same element have one.