Kern 0.12.0 GitHub
Data

<kern-table>

Dense data table: wraps a native <table> (light DOM, so cells can hold any markup and Angular bindings). Sortable column: <th sortable>; sorted="asc|desc" is the current direction (the table sets aria-sort), data-key names the column (else its index). <td data-value="-71"> sorts by that value instead of the cell text. Cells: class="mono" (IP, MAC, IDs), class="num" (right-aligned figures). Rows: class="clickable", aria-selected="true", class="dim" (reset/inactive). Empty state: <td colspan="…" class="empty">.

Data mode: with columns (and rows) the table builds its own <table>; don't combine it with own markup. Assign new arrays to update it. Rows are kept per key (see rowKey): new rows move and update the existing <tr>, so focus, text selection and open details stay; only cells whose class or content changed are touched.

Pagination: page-size adds a kern-pagination below the rows. internal pages the rows itself (back to page 1 after sorting); external shows the rows it gets and the app loads a page on kernPageChange (bubbles from the embedded pagination) and sets total. Labels: set them on table.querySelector('kern-pagination').

Selection: selectable adds a first column with a kern-checkbox per row and one in the header for the visible rows of the current page. Rows are identified by data-key on the <tr> (data mode: rowKey, default row.id or the object identity). selected holds the keys and survives sorting, paging and new rows; selected rows get aria-selected="true". For data loaded from a server use rowKey or id: without both, new objects get new keys.

Detail rows: a <tr kern-details> right after its row is hidden until the row is expanded; it spans all columns, moves with its row when sorting and is no row for paging or sorting. A <button kern-expand> in the row toggles it (the table sets aria-expanded and aria-controls). Data mode: detail adds a chevron column and the detail rows. expanded holds the keys of the open rows.

Column state (data mode): columnState shows, hides, orders and pins columns by key (e.g. from kern-table-columns or a saved view); the rows, their keys, selection and details stay. Pinned columns stay at the start or end while the table scrolls sideways; with columns pinned to the start, the selection and toggle columns stay too. Own markup: no column state, render the columns you want yourself.

Examples

Device table

kern-table wraps a native <table> in the light DOM, so cells can hold any markup. Use class="mono" for IPs, MACs and IDs, class="num" for right-aligned figures, and class="clickable", aria-selected="true" or class="dim" on rows.

NameModelIPMACSignalStatusPower
Bathroom fanPlus 1PM 192.168.1.21 C82B961A3F00 Active 42 W
Kitchen light UpdateShelly 1 192.168.1.34 A4CF12F45B21 Active 9 W
Shutter southPro 2PM 192.168.1.52 34987A6E01C4 Missing –
Garage doorPlus Uni – E86BEA112B90 – Reset –
<kern-table> <table> <thead> <tr><th>Name</th><th>Model</th><th>IP</th><th>MAC</th><th>Signal</th><th>Status</th><th class="num">Power</th></tr> </thead> <tbody> <tr class="clickable"> <td>Bathroom fan</td><td>Plus 1PM</td> <td class="mono">192.168.1.21</td> <td class="mono"><kern-copy>C82B961A3F00</kern-copy></td> <td><kern-signal rssi="-58" hide-value label="Wi-Fi signal"></kern-signal></td> <td><kern-status tone="ok">Active</kern-status></td> <td class="num">42 W</td> </tr> <tr class="clickable" aria-selected="true"> <td>Kitchen light <kern-badge tone="warn">Update</kern-badge></td><td>Shelly 1</td> <td class="mono">192.168.1.34</td> <td class="mono"><kern-copy>A4CF12F45B21</kern-copy></td> <td><kern-signal rssi="-71" hide-value label="Wi-Fi signal"></kern-signal></td> <td><kern-status tone="ok">Active</kern-status></td> <td class="num">9 W</td> </tr> <tr class="clickable"> <td>Shutter south</td><td>Pro 2PM</td> <td class="mono">192.168.1.52</td> <td class="mono"><kern-copy>34987A6E01C4</kern-copy></td> <td><kern-signal rssi="-84" hide-value label="Wi-Fi signal"></kern-signal></td> <td><kern-status tone="warn">Missing</kern-status></td> <td class="num">–</td> </tr> <tr class="dim"> <td>Garage door</td><td>Plus Uni</td> <td class="mono">–</td> <td class="mono">E86BEA112B90</td> <td>–</td> <td><kern-status>Reset</kern-status></td> <td class="num">–</td> </tr> </tbody> </table> </kern-table>

Sortable columns

Mark a header with sortable and the table sorts its rows itself: numbers numerically, text in the page language, empty cells (and –) last. sorted="asc|desc" sets the initial order; the table keeps it and aria-sort up to date. Where a cell shows a component instead of its value, put the value in data-value. compact makes rows 30px instead of 36px.

Name IP Signal Uptime (h)
Kitchen light192.168.1.34 412
Bathroom fan192.168.1.21 99
Shutter south192.168.1.52 7
Garage door192.168.1.9 – –
<kern-table compact> <table> <thead> <tr> <th sortable sorted="asc">Name</th> <th sortable>IP</th> <th sortable>Signal</th> <th sortable class="num">Uptime (h)</th> </tr> </thead> <tbody> <tr> <td>Kitchen light</td><td class="mono">192.168.1.34</td> <td data-value="-71"><kern-signal rssi="-71" hide-value label="Wi-Fi signal"></kern-signal></td> <td class="num">412</td> </tr> <tr> <td>Bathroom fan</td><td class="mono">192.168.1.21</td> <td data-value="-58"><kern-signal rssi="-58" hide-value label="Wi-Fi signal"></kern-signal></td> <td class="num">99</td> </tr> <tr> <td>Shutter south</td><td class="mono">192.168.1.52</td> <td data-value="-84"><kern-signal rssi="-84" hide-value label="Wi-Fi signal"></kern-signal></td> <td class="num">7</td> </tr> <tr> <td>Garage door</td><td class="mono">192.168.1.9</td> <td>–</td> <td class="num">–</td> </tr> </tbody> </table> </kern-table>

Controlled from outside

With controlled="external" the table does not touch the rows. A click on a sortable header (or sortBy()) only emits kernSort with { key, column, direction }; your code loads or orders the data and sets sorted on the header. Use this when the server sorts, or when a framework renders the rows (Angular @for). data-key names a column, otherwise key is its index.

Name Power (W)
<kern-table controlled="external"> <table> <thead> <tr> <th sortable data-key="name">Name</th> <th sortable data-key="power" sorted="desc" class="num">Power (W)</th> </tr> </thead> <tbody></tbody> </table> </kern-table> const devices = [ { name: 'Bathroom fan', power: 42 }, { name: 'Kitchen light', power: 9 }, { name: 'Heat pump', power: 1840 }, ]; const table = root.querySelector('kern-table'); const render = (key, direction) => { const sorted = [...devices].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0) * (direction === 'asc' ? 1 : -1)); root.querySelector('tbody').innerHTML = sorted.map((d) => `<tr><td>${d.name}</td><td class="num">${d.power}</td></tr>`).join(''); root.querySelectorAll('th').forEach((th) => (th.dataset.key === key ? th.setAttribute('sorted', direction) : th.removeAttribute('sorted'))); }; table.addEventListener('kernSort', (ev) => render(ev.detail.key, ev.detail.direction)); render('power', 'desc');

Empty state

When a filter matches nothing, render a single cell spanning all columns with class="empty".

NameIPStatus
No devices match “garage”.
<kern-table> <table> <thead> <tr><th>Name</th><th>IP</th><th>Status</th></tr> </thead> <tbody> <tr><td colspan="3" class="empty">No devices match “garage”.</td></tr> </tbody> </table> </kern-table>

Built from data

Set columns and rows and the table builds its own <table>; don't combine this with own markup. Each column reads row[key]; cell returns a Node (for components like kern-signal) or text, sortValue what to sort by. getRowClass, getCellClass and getHeaderCellClass add classes; kernRowClick reports clicks on a row (not on buttons or fields in it). Assign new arrays to update the table. Sorting, controlled and sortBy() work as with markup.

Click a row.

<kern-table></kern-table> <p class="last-click">Click a row.</p> const el = (tag, attrs, text) => Object.assign(document.createElement(tag), attrs, text ? { textContent: text } : {}); const table = root.querySelector('kern-table'); table.columns = [ { key: 'name', label: 'Name', sortable: true, sorted: 'asc' }, { key: 'ip', label: 'IP', sortable: true, mono: true }, { key: 'rssi', label: 'Signal', sortable: true, cell: (d) => (d.rssi == null ? '–' : Object.assign(el('kern-signal'), { rssi: d.rssi, hideValue: true, label: 'Wi-Fi signal' })), }, { key: 'online', label: 'Status', cell: (d) => Object.assign(el('kern-status', {}, d.online ? 'Active' : 'Missing'), { tone: d.online ? 'ok' : 'warn' }), }, { key: 'power', label: 'Power (W)', sortable: true, align: 'end' }, ]; table.rows = [ { name: 'Kitchen light', ip: '192.168.1.34', rssi: -71, online: true, power: 9 }, { name: 'Bathroom fan', ip: '192.168.1.21', rssi: -58, online: true, power: 42 }, { name: 'Shutter south', ip: '192.168.1.52', rssi: -84, online: false, power: null }, { name: 'Garage door', ip: null, rssi: null, online: false, power: null, reset: true }, ]; table.getRowClass = (d) => (d.reset ? 'dim' : 'clickable'); table.addEventListener('kernRowClick', (ev) => { root.querySelector('.last-click').textContent = `Clicked: ${ev.detail.row.name}`; });

Loading

loading dims the content, shows a bar under the header and sets aria-busy. In data mode without rows it shows loading-rows skeleton rows instead; label-empty is the text for empty data. loading works with markup too.

Reload
<kern-button>Reload</kern-button> <kern-table loading></kern-table> const table = root.querySelector('kern-table'); const data = [ { name: 'Kitchen light', uptime: 412 }, { name: 'Bathroom fan', uptime: 99 }, ]; table.columns = [ { key: 'name', label: 'Name', sortable: true }, { key: 'uptime', label: 'Uptime (h)', sortable: true, align: 'end' }, ]; table.rows = []; const load = () => { table.loading = true; setTimeout(() => { table.rows = data.map((d) => ({ ...d, uptime: d.uptime + Math.floor(Math.random() * 5) })); table.loading = false; }, 1500); }; root.querySelector('kern-button').addEventListener('click', load); load();

Pagination

page-size adds a kern-pagination below the rows; page-sizes offers other sizes. The table pages its rows itself, with data or with own markup, and goes back to page 1 after sorting. kernPageChange bubbles from the embedded pagination; set its labels on table.querySelector('kern-pagination').

<kern-table compact page-size="5" page-sizes="5,10,25"></kern-table> const table = root.querySelector('kern-table'); const rooms = ['Kitchen', 'Bathroom', 'Garage', 'Office', 'Bedroom', 'Hallway', 'Garden', 'Attic']; const kinds = ['light', 'fan', 'shutter']; table.columns = [ { key: 'name', label: 'Name', sortable: true, sorted: 'asc' }, { key: 'ip', label: 'IP', sortable: true, mono: true }, { key: 'power', label: 'Power (W)', sortable: true, align: 'end' }, ]; table.rows = Array.from({ length: 23 }, (_, i) => ({ name: `${rooms[i % rooms.length]} ${kinds[i % kinds.length]}`, ip: `192.168.1.${20 + i}`, power: (i * 37) % 120, }));

Paging on the server

With controlled="external" the table shows the rows it gets and only reports kernPageChange and kernSort. Your code loads the page and sets rows, total and page (usually back to 1 after sorting). Here a timeout stands in for the server.

<kern-table controlled="external" page-size="5" page-sizes="5,10"></kern-table> const table = root.querySelector('kern-table'); const all = Array.from({ length: 42 }, (_, i) => ({ id: `SH-${String(i + 1).padStart(3, '0')}`, uptime: (i * 53) % 900 })); const state = { page: 1, pageSize: 5, key: 'id', direction: 'asc' }; const columns = () => [ { key: 'id', label: 'Device ID', sortable: true, mono: true }, { key: 'uptime', label: 'Uptime (h)', sortable: true, align: 'end' }, ].map((c) => ({ ...c, sorted: c.key === state.key ? state.direction : undefined })); const load = () => { table.loading = true; setTimeout(() => { // “Server”: sort, then cut the page const sign = state.direction === 'asc' ? 1 : -1; const sorted = [...all].sort((a, b) => (a[state.key] > b[state.key] ? 1 : a[state.key] < b[state.key] ? -1 : 0) * sign); const start = (state.page - 1) * state.pageSize; table.columns = columns(); table.total = all.length; table.page = state.page; table.rows = sorted.slice(start, start + state.pageSize); table.loading = false; }, 400); }; table.addEventListener('kernPageChange', (ev) => { Object.assign(state, ev.detail); load(); }); table.addEventListener('kernSort', (ev) => { Object.assign(state, { key: ev.detail.key, direction: ev.detail.direction, page: 1 }); load(); }); load();

Selectable rows

selectable adds a first column with a checkbox per row. The header checkbox selects or clears the visible rows of the current page and shows a mixed state when only some of them are selected. Rows are identified by data-key on the <tr>; selected holds the keys and survives sorting, paging and new rows, and selected rows get aria-selected="true". kernSelectionChange reports the new keys (cancel it to keep selected as it is). A click on a checkbox is no row click. label-select-all and label-select-row name the checkboxes for screen readers.

NameIPPower (W)
Kitchen light192.168.1.349
Bathroom fan192.168.1.2142
Shutter south192.168.1.523
Garage door192.168.1.90
Office plug192.168.1.61120

No devices selected.

<kern-table selectable compact page-size="3"> <table> <thead> <tr><th sortable sorted="asc">Name</th><th sortable>IP</th><th sortable class="num">Power (W)</th></tr> </thead> <tbody> <tr data-key="A4CF12F45B21"><td>Kitchen light</td><td class="mono">192.168.1.34</td><td class="num">9</td></tr> <tr data-key="C82B961A3F00"><td>Bathroom fan</td><td class="mono">192.168.1.21</td><td class="num">42</td></tr> <tr data-key="34987A6E01C4"><td>Shutter south</td><td class="mono">192.168.1.52</td><td class="num">3</td></tr> <tr data-key="E86BEA112B90"><td>Garage door</td><td class="mono">192.168.1.9</td><td class="num">0</td></tr> <tr data-key="B0A73291C5E8"><td>Office plug</td><td class="mono">192.168.1.61</td><td class="num">120</td></tr> </tbody> </table> </kern-table> <p class="selection">No devices selected.</p> root.querySelector('kern-table').addEventListener('kernSelectionChange', (ev) => { const n = ev.detail.selected.length; root.querySelector('.selection').textContent = n ? `${n} selected: ${ev.detail.selected.join(', ')}` : 'No devices selected.'; });

Selectable rows from data

In data mode rowKey returns a row's key (default: row.id, else a key per row object). For data loaded from a server set rowKey or give rows an id: without both, the key belongs to the object, so a selection follows it through sorting and filtering, but reloaded objects are not selected. Set selected to select rows from the app, e.g. for a bulk action like “only devices with changes”; the table keeps keys of rows that are filtered out.

Only with update None 0 selected
<kern-toolbar> <kern-button size="sm" class="updates">Only with update</kern-button> <kern-button size="sm" variant="ghost" class="none">None</kern-button> <span class="count" slot="end">0 selected</span> </kern-toolbar> <kern-table selectable compact></kern-table> const table = root.querySelector('kern-table'); const devices = [ { mac: 'A4CF12F45B21', name: 'Kitchen light', firmware: '1.4.2', update: true }, { mac: 'C82B961A3F00', name: 'Bathroom fan', firmware: '1.5.0', update: false }, { mac: '34987A6E01C4', name: 'Shutter south', firmware: '1.3.9', update: true }, { mac: 'E86BEA112B90', name: 'Garage door', firmware: '1.5.0', update: false }, ]; table.columns = [ { key: 'name', label: 'Name', sortable: true, sorted: 'asc' }, { key: 'mac', label: 'MAC', mono: true }, { key: 'firmware', label: 'Firmware', sortable: true, mono: true }, ]; table.rows = devices; table.rowKey = (d) => d.mac; const select = (keys) => { table.selected = keys; root.querySelector('.count').textContent = `${keys.length} selected`; }; table.addEventListener('kernSelectionChange', (ev) => select(ev.detail.selected)); root.querySelector('.updates').addEventListener('click', () => select(devices.filter((d) => d.update).map((d) => d.mac))); root.querySelector('.none').addEventListener('click', () => select([]));

Detail rows

A <tr kern-details> right after a row is its detail row: hidden until the row is expanded, spanning all columns (the selection column, too). It moves with its row when sorting, doesn't count as a row for paging and hides with its row. A <button kern-expand> in the row toggles it; the table sets aria-expanded and aria-controls. expanded holds the keys of the open rows (data-key), kernRowToggle reports { key, open } (cancel it to keep expanded), and toggleRow(key, open?) opens or closes a row from the app.

DeviceChangesTemplate
Kitchen light2
on → off off → on
Bathroom fan0–
Shutter south1
on → off
Office plug3
Plug → Office plug last → off on → off

Open all

<kern-table selectable compact page-size="3"> <table> <thead> <tr><th sortable sorted="asc">Device</th><th sortable class="num">Changes</th><th>Template</th></tr> </thead> <tbody> <tr data-key="A4CF12F45B21"> <td>Kitchen light</td><td class="num">2</td> <td><button kern-expand>Preview <kern-icon name="chevron-right"></kern-icon></button></td> </tr> <tr kern-details><td> <kern-kv> <kern-kv-item label="Wi-Fi AP">on → off</kern-kv-item> <kern-kv-item label="Eco mode">off → on</kern-kv-item> </kern-kv> </td></tr> <tr data-key="C82B961A3F00"> <td>Bathroom fan</td><td class="num">0</td><td>–</td> </tr> <tr data-key="34987A6E01C4"> <td>Shutter south</td><td class="num">1</td> <td><button kern-expand>Preview <kern-icon name="chevron-right"></kern-icon></button></td> </tr> <tr kern-details><td> <kern-kv><kern-kv-item label="Cloud">on → off</kern-kv-item></kern-kv> </td></tr> <tr data-key="B0A73291C5E8"> <td>Office plug</td><td class="num">3</td> <td><button kern-expand>Preview <kern-icon name="chevron-right"></kern-icon></button></td> </tr> <tr kern-details><td> <kern-kv> <kern-kv-item label="Name">Plug → Office plug</kern-kv-item> <kern-kv-item label="Power on">last → off</kern-kv-item> <kern-kv-item label="Cloud">on → off</kern-kv-item> </kern-kv> </td></tr> </tbody> </table> </kern-table> <p><kern-button size="sm" class="all">Open all</kern-button></p> const table = root.querySelector('kern-table'); root.querySelector('.all').addEventListener('click', () => { table.expanded = ['A4CF12F45B21', '34987A6E01C4', 'B0A73291C5E8']; });

Detail rows from data

In data mode detail returns the content of a row's detail row (a Node or text) and adds a first column with a chevron toggle. It is called when a row is opened for the first time. label-expand-row names the toggle for screen readers. expanded uses the same keys as selected: rowKey or row.id (use one of them for server data), else a key per row object.

<kern-table compact></kern-table> const table = root.querySelector('kern-table'); table.columns = [ { key: 'time', label: 'Time', sortable: true, sorted: 'desc', mono: true }, { key: 'device', label: 'Device', sortable: true }, { key: 'message', label: 'Message' }, ]; table.rows = [ { id: 1, time: '14:02:11', device: 'Kitchen light', message: 'Firmware update finished', data: { from: '1.4.2', to: '1.5.0' } }, { id: 2, time: '13:58:40', device: 'Shutter south', message: 'Connection lost', data: { rssi: -88, retries: 3 } }, { id: 3, time: '13:12:05', device: 'Bathroom fan', message: 'Config changed', data: { eco: true } }, ]; table.detail = (row) => Object.assign(document.createElement('kern-json'), { data: row.data });

Live data

New rows don't rebuild the table: rows are kept per key (rowKey, else row.id) and moved into the new order, and only cells whose text or class changed are touched. A focused button in a cell, selected text, open detail rows and their scroll position survive updates, even several per second. Rows that are no longer shown (removed, filtered, on another page) are dropped with their nodes.

cell is called on every update. Its third argument is the Node the cell shows now: change it and return it to keep it, or return a new Node to replace it. Text is only written when it changed. detail is called once, when a row is opened; update its content yourself. Here the progress bars and buttons are created once per row. Sort by progress and focus a button: the rows move every second, the button keeps the focus and the open log its scroll position.

<kern-table compact></kern-table> const el = (tag, props) => Object.assign(document.createElement(tag), props); const table = root.querySelector('kern-table'); let jobs = [ { id: 'k', name: 'Kitchen light', progress: 12 }, { id: 'b', name: 'Bathroom fan', progress: 48 }, { id: 's', name: 'Shutter south', progress: 73 }, ]; table.columns = [ { key: 'name', label: 'Device', sortable: true }, { key: 'progress', label: 'Progress', sortable: true, width: '40%', // Created once per row, then only its value changes. cell: (job, _i, bar = el('kern-progress', { label: job.name })) => Object.assign(bar, { value: job.progress, valueText: `${job.progress} %` }), }, { key: 'action', label: 'Action', // The same button on every update: it keeps the focus while its row moves. cell: (job, _i, button = el('kern-button', { size: 'sm', textContent: 'Restart' })) => button, }, ]; table.detail = (job) => el('kern-code', { heading: 'Log', maxHeight: '120px', code: Array.from({ length: 20 }, (_, i) => `${job.name}: step ${i + 1}`).join('\n') }); table.rows = jobs; const timer = setInterval(() => { if (!table.isConnected) return clearInterval(timer); jobs = jobs.map((job) => ({ ...job, progress: job.progress >= 100 ? 0 : Math.min(100, job.progress + Math.ceil(Math.random() * 6)) })); table.rows = jobs; }, 1000);

Column state

In data mode columnState shows, hides, orders and pins columns by their key: [{ key, hidden?, pinned? }], the array order is the column order. It is plain data, so an app can store it (see kern-table-columns and the table views pattern). Columns the state doesn't name keep their place in columns, unknown keys are ignored, so a state stored before a column was added or removed still works. Without a state the table uses columns[].hidden and columns[].pinned.

  • pinned: 'start' | 'end' keeps a column in view while the table scrolls sideways. With columns pinned to the start, the selection and detail toggle columns stay in view before them.
  • minWidth keeps a column from shrinking; the table scrolls sideways instead. hideable: false keeps a column always visible, min-visible-columns keeps at least that many.
  • Rows, their keys, selection, open details, sorting (also by a hidden column) and the page stay when the state changes; cells move within their rows.

Own markup has no column state: render the columns you want yourself (e.g. with @if in Angular). Keys of header cells with colspan/rowspan would not name one column, so the table doesn't hide or move them.

Diagnosis Maintenance Default

<kern-toolbar> <kern-button size="sm" class="diagnose">Diagnosis</kern-button> <kern-button size="sm" class="maintain">Maintenance</kern-button> <kern-button size="sm" variant="ghost" class="reset">Default</kern-button> </kern-toolbar> <div style="display:grid;gap:var(--space-3);max-width:560px"> <kern-table selectable compact></kern-table> <pre class="state" style="margin:0;font:12px/18px var(--font-mono);color:var(--text-muted);white-space:pre-wrap"></pre> </div> const table = root.querySelector('kern-table'); table.columns = [ { key: 'name', label: 'Name', sortable: true, sorted: 'asc', hideable: false, pinned: 'start', minWidth: '140px' }, { key: 'ip', label: 'IP', mono: true }, { key: 'lastSeen', label: 'Last seen', sortable: true }, { key: 'model', label: 'Model', minWidth: '160px' }, { key: 'firmware', label: 'Firmware', mono: true, hidden: true }, { key: 'update', label: 'Update', hidden: true }, { key: 'site', label: 'Site', minWidth: '120px' }, { key: 'power', label: 'Power (W)', sortable: true, align: 'end', pinned: 'end' }, ]; table.rows = [ { id: 'a1', name: 'Kitchen light', ip: '192.168.1.34', lastSeen: '2 min ago', model: 'Shelly 1 Mini Gen3', firmware: '1.4.4', update: '–', site: 'Haus Seeblick', power: 9 }, { id: 'a2', name: 'Bathroom fan', ip: '192.168.1.21', lastSeen: '1 min ago', model: 'Shelly Plus 1PM', firmware: '1.4.2', update: '1.4.4', site: 'Haus Seeblick', power: 42 }, { id: 'a3', name: 'Shutter south', ip: '192.168.1.52', lastSeen: '3 days ago', model: 'Shelly Plus 2PM', firmware: '1.3.3', update: '1.4.4', site: 'Ferienwohnung', power: null }, ]; const show = (state) => { table.columnState = state; root.querySelector('.state').textContent = JSON.stringify(state ?? 'default'); }; root.querySelector('.diagnose').addEventListener('click', () => show([{ key: 'name', pinned: 'start' }, { key: 'ip' }, { key: 'lastSeen' }, { key: 'model', hidden: true }, { key: 'site' }]), ); root.querySelector('.maintain').addEventListener('click', () => show([{ key: 'name', pinned: 'start' }, { key: 'firmware' }, { key: 'update' }, { key: 'model' }, { key: 'ip', hidden: true }, { key: 'lastSeen', hidden: true }]), ); root.querySelector('.reset').addEventListener('click', () => show(undefined)); show(undefined);

Properties

columnState
KernColumnState[] | undefined

Data mode: which columns show, in which order, and which are pinned, by key; plain data to store. Columns it doesn't name keep their place in columns, unknown keys are ignored. Default: columns[].hidden and .pinned.

columns
KernTableColumn<any>[] | undefined

Data mode: column definitions. The table builds its own <table> from them and rows.

compactreflectsdefault false
boolean

30px rows instead of 36px.

controlledreflectsdefault 'internal'
"external" | "internal"

internal: the table sorts its rows itself. external: it only emits kernSort; the app orders the rows and sets sorted on the header (in data mode: new rows and columns[].sorted).

detail
((row: any, index: number) => string | Node) | undefined

Data mode: content of a row's detail row; adds a first column with a toggle. Called when the row is first opened; the content stays while the row is shown (new rows don't rebuild it), so update it yourself for live data.

expandedmutabledefault []
string[]

Keys of the expanded rows (see selected and rowKey for keys).

getCellClass
((row: any, column: KernTableColumn<any>, index: number) => string | undefined) | undefined

Data mode: classes for a cell.

getHeaderCellClass
((column: KernTableColumn<any>) => string | undefined) | undefined

Data mode: classes for a header cell.

getRowClass
((row: any, index: number) => string | undefined) | undefined

Data mode: classes for a row, e.g. clickable or dim.

labelEmptyattribute label-empty · default No entries · de Keine Einträge
string | undefined

Data mode: shown when rows is empty.

labelExpandRowattribute label-expand-row · default Show details · de Details anzeigen
string | undefined

Data mode: accessible name of the toggle button of a row.

labelSelectAllattribute label-select-all · default Select all · de Alle auswählen
string | undefined

Accessible name of the header checkbox.

labelSelectRowattribute label-select-row · default Select row · de Zeile auswählen
string | undefined

Accessible name of a row checkbox.

loadingreflectsdefault false
boolean

Loading: content dimmed, a bar under the header, aria-busy; in data mode without rows, skeleton rows.

loadingRowsattribute loading-rows · default 3
number

Data mode: number of skeleton rows while loading without rows.

minVisibleColumnsattribute min-visible-columns · default 1
number

Data mode: at least this many columns stay visible; a column state hiding more shows the first hidden ones again.

pagemutabledefault 1
number

Current page, from 1.

pageSizemutableattribute page-size
number | undefined

Rows per page; turns the pagination on.

pageSizesattribute page-sizes · default ''
number[] | string

Offered page sizes, e.g. 10,25,50; empty hides the choice.

rowKey
((row: any, index: number) => string | number) | undefined

Data mode: key of a row for selected, expanded and keeping its <tr> through updates. Default: row.id, else a key per row object (kept through sorting, filtering and reordering, but new for reloaded objects; use rowKey or id for server data). Rows that are no objects use their index in rows.

rows
any[] | undefined

Data mode: one object per row; each column reads row[column.key].

selectablereflectsdefault false
boolean

Adds a first column with a checkbox per row and a header checkbox for the visible rows of the current page.

selectedmutabledefault []
string[]

Keys of the selected rows (data-key, in data mode rowKey). Keys of rows not shown (filtered, other page) stay. Data mode without rowKey and id: the key belongs to the row object, so reloaded objects are not selected.

total
number | undefined

controlled="external": number of all rows (the table only gets the current page).

Events

kernRowClick
CustomEvent<{ row: any; index: number; }>

Data mode: a row was clicked (not on a button, link or field in it). index is the position in rows.

kernRowToggle
CustomEvent<{ key: string; open: boolean; }>

A row's details were opened or closed (button or toggleRow()). Cancelable: preventDefault() keeps expanded as it is.

kernSelectionChange
CustomEvent<{ selected: string[]; }>

The selection was changed with a checkbox. Cancelable: preventDefault() keeps selected as it is.

kernSort
CustomEvent<{ key: string; column: number; direction: KernSortDirection; }>

A sortable header was activated (or sortBy() called), with the requested direction. Cancelable: preventDefault() skips the built-in sort.

Methods

sortBy()
sortBy(column: string | number, direction?: KernSortDirection) => Promise<void>

Sorts by a column (data-key or index); without direction it toggles, starting with asc. With controlled="external" it only emits kernSort.

toggleRow()
toggleRow(key: string, open?: boolean) => Promise<void>

Opens or closes a row's details (open omitted: toggles). Emits kernRowToggle.

Slots

(default)

A native <table> (not in data mode)