GitHub

Components / Display & Media / Data Table

Data Table beta

The table with its working clothes on: sortable columns, selectable rows, and an honest footer.

Preview
Role Status
Aoi KimuraDesignerActive$4,820
Ren SatoEngineerActive$3,950
Mei ChenEngineerInvited$0
Jonas BergSupportActive$2,715
Priya NairDesignerSuspended$1,980
5 members

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