Data table
Data table renders a table from your rows and column definitions, with sorting, column resizing, row selection, expandable rows, and pagination.
<tk-data-table label="Quarterly revenue by region" hover></tk-data-table>
<tk-data-table id="revenue" label="Quarterly revenue by region" hover></tk-data-table>
<script>
const table = document.getElementById('revenue');
table.columns = [
{ key: 'region', header: 'Region', sortable: true },
{ key: 'lead', header: 'Lead', resizable: true, width: 220 },
{ key: 'revenue', header: 'Revenue', numeric: true, sortable: true },
];
table.data = [
{ id: 'emea', region: 'EMEA', lead: 'Alice Chen', revenue: 1204 },
{ id: 'apac', region: 'APAC', lead: 'Bo Nguyen', revenue: 987 },
{ id: 'amer', region: 'Americas', lead: 'Carla Diaz', revenue: 2130 },
];
</script>Anatomy
| Part | What it is |
|---|---|
tk-data-table |
the element; takes data and columns as JS properties |
| caption | hidden; it holds label and names the table |
| sort button | a sortable column's header |
| resize handle | the edge of a resizable column; drag it or use the arrow keys |
| selection column | a "select all" checkbox in the header and one checkbox per row |
| expand button | a chevron in the first cell of a row with children |
scroll part |
the scroll container |
Columns
Each column is an object:
{
key: string; // the field on each row, and the column id
header: string; // the header text
align?: 'start' | 'center' | 'end';
numeric?: boolean; // end-aligned, tabular figures
sortable?: boolean;
resizable?: boolean;
width?: number; // initial width in px; unset shares the free space
minWidth?: number; // resize limit, default 48
maxWidth?: number; // resize limit, default 800
}Leave at least one column without width so the table fills its container. If every
column has a width, the table is exactly as wide as their sum.
Sorting
Set sortable: true on a column. Clicking its header cycles ascending, descending, and
unsorted. Read the current sort from sort.
Each click fires tk-sort first. Call preventDefault() on it to stop the table from
sorting. The header then keeps its previous sort indicator.
table.addEventListener('tk-sort', (e) => {
console.log(e.detail); // { key: 'revenue', direction: 'asc' }
});Column resizing
Set resizable: true on a column. Drag the handle at its edge, or focus the handle and
use the arrow keys: ← and → change the width by 8px, Home and End jump to
minWidth and maxWidth. While you hover, focus, or drag a handle, a line runs down the
whole table at the column edge.
Each finished change fires tk-column-resize. Read the widths from columnSizing.
Row selection
selectable adds a checkbox to every row and a "select all" checkbox to the header.
"Select all" selects every row, on every page. Selecting a parent row selects its children.
Set row-key to a field that is unique and stable on every row; selection and expanded
rows are stored by it. Read the selection from selectedKeys, and listen for
tk-selection-change.
Expandable rows
Set expandable and put child rows under children, or the field you name in
children-key. Rows with children get an expand button, and their children are indented.
Children sort inside their parent.
table.expandable = true;
table.data = [
{ id: 'emea', name: 'EMEA', headcount: 120, children: [
{ id: 'uk', name: 'United Kingdom', headcount: 45 },
]},
{ id: 'amer', name: 'Americas', headcount: 200 },
];Pagination
Set page-size and only the current page renders. The page controls are a
pageCount, and set page (counted from 0) when one is selected.
<tk-data-table id="records" label="Records" page-size="10"></tk-data-table>
<tk-pagination id="pages" label="Records pages"></tk-pagination>
<script>
const table = document.getElementById('records');
const pages = document.getElementById('pages');
const renderPages = () => {
pages.innerHTML = Array.from({ length: table.pageCount }, (_, i) =>
`<tk-pagination-item value="${i + 1}" ${i === table.page ? 'current' : ''}>${i + 1}</tk-pagination-item>`,
).join('');
};
pages.addEventListener('tk-page-select', (e) => {
table.page = Number(e.detail.value) - 1;
renderPages();
});
renderPages();
</script>Column order
Set columnOrder to an array of column keys to reorder the columns. People can't drag
columns to reorder them.
When to use
- Use a data table when the table does something: sorts, selects rows, expands rows, or pages.
- Use a
Table for a static table you write by hand. It looks the same. - Use a
Tree for a hierarchy without columns, such as a file browser. With columns, use a data table withexpandable. - Your app owns the data. Data table keeps the view state (sort, selection, expanded rows, widths, page) and never fetches or changes your rows.
Do / Don't
| ✅ Do | ❌ Don't |
|---|---|
| Use a data table when rows sort, select, expand, or page | Use it for a static table; use |
Leave one column without width |
Fix every width, unless the table should have a fixed width |
Set row-key to a stable id |
Rely on row position when the data changes |
Replace data with a new array |
Change rows inside the array in place |
Use expandable with nested children |
Build a separate tree table |
Related
Table : the same look for a table you write by hand.Pagination : the page controls a paged table uses.Tree : a hierarchy without columns.Checkbox : the control in the selection column.
API
Properties
| Property | Type | What it does |
|---|---|---|
data |
object[] |
the rows; never changed by the table. Replace the array to update |
columns |
ColumnDef[] |
the column definitions; see |
columnOrder |
string[] |
the column order, as column keys |
Attributes
| Attribute | Type | Default | What it does |
|---|---|---|---|
label |
string | the table's name, required | |
density |
comfortable | compact |
comfortable |
cell padding, as in |
zebra |
boolean | false |
shades every other row |
hover |
boolean | false |
highlights the row under the pointer |
header-fill |
boolean | false |
fills the header row |
sort-shape |
rect | round |
rect |
the sort button's corners: rounded or a pill |
selectable |
boolean | false |
adds the selection column |
expandable |
boolean | false |
rows with children get an expand button |
children-key |
string | children |
the field that holds a row's children |
row-key |
string | id |
the field that identifies a row |
select-row-label |
string | Select {row} |
a row checkbox's name; {row} is the first cell |
select-all-label |
string | Select all rows |
the header checkbox's name |
resize-column-label |
string | Resize column: {column} |
a resize handle's name; {column} is the header |
page-size |
number | rows per page; unset shows every row | |
page |
number | 0 |
the current page, counted from 0 |
Read-only properties
| Property | Type | What it holds |
|---|---|---|
sort |
{ key, direction } | null |
the current sort |
selectedKeys |
string[] |
the selected rows' keys |
expandedKeys |
string[] |
the expanded rows' keys |
pageCount |
number | the number of pages; 1 without page-size |
columnSizing |
Record<string, number> |
column widths in px, after a resize |
Events
| Event | Detail | Cancelable | Fires when |
|---|---|---|---|
tk-sort |
{ key, direction }; direction is asc, desc, or null |
yes | a sort button is clicked |
tk-selection-change |
{ selectedKeys, key, selected }; key is all for the header |
yes | a checkbox is toggled |
tk-row-toggle |
{ key, expanded } |
yes | an expand button is clicked |
tk-column-resize |
{ key, width } |
no | a resize ends, or with each arrow key |
All events bubble out of the element. preventDefault() on a cancelable event stops the
change.
CSS
::part(scroll): the scroll container. Setmax-heighton it to limit the table's height.
Tokens
Data table uses the
| Element | Tokens |
|---|---|
| sort button | hover state/hover, press state/press |
| sort arrow | icon/secondary; icon/default on the sorted column |
| resize line | border/default; border/accent on hover, focus, and drag |
| selected row | state/on; hover state/on-hover |
| expand button | icon/secondary; hover state/hover + icon/default |
| child row indent | space-24 per level |
Focus rings are border/accent/focus. Values per theme:
Accessibility
Built in:
- A native table: screen readers get real rows, column headers, and cells, and
labelnames the table. - A sort button is a real button named by its header; the sorted column has
aria-sort. - A resize handle is a focusable separator named "Resize column: {header}", with its width as the value.
- Checkboxes are named "Select all rows" and "Select {row}". The header checkbox shows a mixed state when some rows are selected.
- An expand button is named by its row and has
aria-expanded. Rows havearia-level, and collapsed children aren't rendered. - When the table is wider than its container, the scroll container becomes a Tab stop and a named region.
Your part:
- Always set
label. - Give every row the
row-keyfield (idby default). Rows without it are keyed by position, and the selection moves to other rows when the data changes. - Translate
select-row-label,select-all-label, andresize-column-labelfor other languages. - Don't put meaning in row color. The checkbox carries selection.
| Key | Action |
|---|---|
Tab |
moves through sort buttons, resize handles, checkboxes, and expand buttons |
Enter / Space |
activates a sort button or expand button; Space toggles a checkbox |
← / → |
on a resize handle, narrows or widens the column by 8px |
Home / End |
on a resize handle, jumps to the narrowest or widest width |