Components / Display & Media / Data Table
Data Table beta
The table with its working clothes on: sortable columns, selectable rows, and an honest footer.
| Role | Status | |||
|---|---|---|---|---|
| Aoi Kimura | Designer | Active | $4,820 | |
| Ren Sato | Engineer | Active | $3,950 | |
| Mei Chen | Engineer | Invited | $0 | |
| Jonas Berg | Support | Active | $2,715 | |
| Priya Nair | Designer | Suspended | $1,980 |
Uses: Badge, Button — include its CSS alongside this one.
<div class="k-dt" data-dt style="max-width:560px">
<table class="k-dt-t">
<thead><tr>
<th style="width:34px"><input type="checkbox" aria-label="Select all rows"></th>
<th aria-sort="ascending"><button class="k-dt-sort" data-sort="str" data-dir="asc" type="button">Name <span class="arr"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></button></th>
<th>Role</th>
<th>Status</th>
<th class="num"><button class="k-dt-sort" data-sort="num" data-dir="" type="button">Billed <span class="arr"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></button></th>
</tr></thead>
<tbody><tr><td><input type="checkbox" aria-label="Select Aoi Kimura"></td><td style="font-weight:550">Aoi Kimura</td><td>Designer</td><td><span class="badge teal">Active</span></td><td class="num" data-v="4820">$4,820</td></tr><tr><td><input type="checkbox" aria-label="Select Ren Sato"></td><td style="font-weight:550">Ren Sato</td><td>Engineer</td><td><span class="badge teal">Active</span></td><td class="num" data-v="3950">$3,950</td></tr><tr><td><input type="checkbox" aria-label="Select Mei Chen"></td><td style="font-weight:550">Mei Chen</td><td>Engineer</td><td><span class="badge">Invited</span></td><td class="num" data-v="0">$0</td></tr><tr><td><input type="checkbox" aria-label="Select Jonas Berg"></td><td style="font-weight:550">Jonas Berg</td><td>Support</td><td><span class="badge teal">Active</span></td><td class="num" data-v="2715">$2,715</td></tr><tr><td><input type="checkbox" aria-label="Select Priya Nair"></td><td style="font-weight:550">Priya Nair</td><td>Designer</td><td><span class="badge red">Suspended</span></td><td class="num" data-v="1980">$1,980</td></tr></tbody>
</table>
<div class="k-dt-f">5 members<span class="pg"><button class="btn btn-sec btn-sm" disabled>Previous</button><button class="btn btn-sec btn-sm">Next</button></span></div>
</div>.k-dt{border:1px solid var(--k-line);border-radius:14px;background:var(--k-bg);font-family:var(--k-font);overflow:hidden;text-align:left;color:var(--k-ink)}
.k-dt-t{width:100%;border-collapse:collapse;font-size:13.5px}
.k-dt-t th{font-size:11px;letter-spacing:.07em;text-transform:uppercase;color:var(--k-ink-3);font-weight:600;text-align:left;padding:11px 14px;border-bottom:1px solid var(--k-line);background:var(--k-bg-soft);white-space:nowrap}
.k-dt-t td{padding:12px 14px;border-bottom:1px solid var(--k-line);vertical-align:middle}
.k-dt-t tbody tr:hover{background:var(--k-bg-soft)}
.k-dt-t tbody tr:last-child td{border-bottom:none}
.k-dt-t .num{text-align:right;font-variant-numeric:tabular-nums}
.k-dt-t th.num{text-align:right}
.k-dt-sort{display:inline-flex;align-items:center;gap:5px;border:none;background:none;font:inherit;color:inherit;cursor:pointer;padding:0;text-transform:inherit;letter-spacing:inherit}
.k-dt-sort .arr{display:grid;place-items:center;opacity:.35;transition:transform .15s var(--k-ease),opacity .15s}
.k-dt-sort[data-dir="asc"] .arr{opacity:1;transform:rotate(180deg)}
.k-dt-sort[data-dir="desc"] .arr{opacity:1}
.k-dt input[type="checkbox"]{accent-color:var(--k-accent);width:15px;height:15px;cursor:pointer;vertical-align:middle}
.k-dt-f{display:flex;align-items:center;gap:10px;padding:11px 14px;border-top:1px solid var(--k-line);font-size:12.5px;color:var(--k-ink-3)}
.k-dt-f .pg{margin-left:auto;display:flex;gap:7px}
const dt = document.querySelector('[data-dt]');
const dtBody = dt.querySelector('tbody');
dt.querySelectorAll('[data-sort]').forEach(btn => {
btn.addEventListener('click', () => {
const th = btn.closest('th');
const col = [...th.parentNode.children].indexOf(th);
const type = btn.dataset.sort;
const dir = btn.dataset.dir === 'asc' ? 'desc' : 'asc';
dt.querySelectorAll('[data-sort]').forEach(b => { b.dataset.dir = ''; b.closest('th').removeAttribute('aria-sort'); });
btn.dataset.dir = dir;
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
const rows = [...dtBody.rows];
rows.sort((a, b) => {
const x = a.cells[col].dataset.v ?? a.cells[col].textContent.trim();
const y = b.cells[col].dataset.v ?? b.cells[col].textContent.trim();
const cmp = type === 'num' ? (+x - +y) : String(x).localeCompare(String(y));
return dir === 'asc' ? cmp : -cmp;
});
rows.forEach(r => dtBody.appendChild(r));
});
});
Usage
For admin lists where the reader compares, sorts and acts. Read-only data stays a plain Table; the moment headers sort and rows select, it is this part.
Accessibility
Sort state lives on aria-sort at the column header; checkboxes carry row names in their labels. Sorting is stable and never reorders under a screen reader without announcing it.
Related parts
Aspect Ratio beta
A box that holds its shape — 16/9 for video, 4/3 for photos, 1/1 for avatars — whatever fills it.
Avatar beta
Initials first, photo when there is one. Three sizes, a status dot, and a stacking group.
Bar Chart beta
Columns for comparing a handful of values — the chart people read correctly at a glance.
Card beta
The universal container: a hairline box with a header, a body and a footer that agree with each other.