Patterns Medium
Sortable Table
Sort plus column-resize table pattern with no selection or pagination complexity.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Sora", system-ui, sans-serif;
background: #090f1f;
color: #e2e8f0;
}
.shell {
width: min(920px, calc(100% - 2rem));
margin: 2rem auto;
}
p {
color: #94a3b8;
}
.table-wrap {
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 12px;
overflow: auto;
background: #10192e;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th,
td {
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
padding: 0.65rem;
text-align: left;
position: relative;
}
th button {
all: unset;
cursor: pointer;
color: #b7c8e4;
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.resize-handle {
position: absolute;
right: 0;
top: 0;
width: 8px;
height: 100%;
cursor: col-resize;
}
.resize-handle::after {
content: "";
position: absolute;
right: 3px;
top: 20%;
height: 60%;
width: 1px;
background: rgba(255, 255, 255, 0.2);
}(() => {
const rows = [
{ name: "Lia", team: "Core", score: 89, updated: "2026-02-28" },
{ name: "Milo", team: "Growth", score: 72, updated: "2026-03-01" },
{ name: "Aya", team: "Core", score: 94, updated: "2026-03-02" },
{ name: "Noah", team: "Ops", score: 66, updated: "2026-02-27" },
{ name: "Ira", team: "Growth", score: 81, updated: "2026-03-03" },
];
let sortKey = "name";
let sortDir = "asc";
const tbody = document.getElementById("body");
const render = () => {
const sorted = rows.slice().sort((a, b) => {
const left = a[sortKey];
const right = b[sortKey];
if (left === right) return 0;
if (typeof left === "number" && typeof right === "number") {
return sortDir === "asc" ? left - right : right - left;
}
return sortDir === "asc"
? String(left).localeCompare(String(right))
: String(right).localeCompare(String(left));
});
tbody.innerHTML = "";
for (const row of sorted) {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${row.name}</td>
<td>${row.team}</td>
<td>${row.score}</td>
<td>${row.updated}</td>
`;
tbody.appendChild(tr);
}
};
const sortButtons = document.querySelectorAll("th[data-key] > button");
for (const button of sortButtons) {
button.addEventListener("click", () => {
const key = button.parentElement?.getAttribute("data-key");
if (!key) return;
if (sortKey === key) {
sortDir = sortDir === "asc" ? "desc" : "asc";
} else {
sortKey = key;
sortDir = "asc";
}
render();
});
}
let activeTh = null;
let startX = 0;
let startWidth = 0;
const onMove = (event) => {
if (!activeTh) return;
const delta = event.clientX - startX;
const nextWidth = Math.max(90, startWidth + delta);
activeTh.style.width = `${nextWidth}px`;
};
const onUp = () => {
activeTh = null;
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
const resizeHandles = document.querySelectorAll("[data-resize]");
for (const handle of resizeHandles) {
handle.addEventListener("mousedown", (event) => {
const th = handle.parentElement;
if (!(th instanceof HTMLElement)) return;
activeTh = th;
startX = event.clientX;
startWidth = th.offsetWidth;
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
event.preventDefault();
});
}
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sortable Table</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<h1>Sortable Resizable Table</h1>
<p>Click headers to sort. Drag the right edge of a header to resize a column.</p>
<div class="table-wrap">
<table id="table">
<thead>
<tr>
<th data-key="name" style="width: 34%">
<button type="button">Name</button>
<span class="resize-handle" data-resize></span>
</th>
<th data-key="team" style="width: 30%">
<button type="button">Team</button>
<span class="resize-handle" data-resize></span>
</th>
<th data-key="score" style="width: 18%">
<button type="button">Score</button>
<span class="resize-handle" data-resize></span>
</th>
<th data-key="updated" style="width: 18%">
<button type="button">Updated</button>
<span class="resize-handle" data-resize></span>
</th>
</tr>
</thead>
<tbody id="body"></tbody>
</table>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Sortable Table
A focused table interaction pattern for analytics views that need column sorting and resizing only.
Features
- Click header to sort ascending and descending
- Drag handle to resize columns
- Numeric and text sorting
- Minimal state model
Notes
This intentionally omits selection, bulk actions, and pagination from data-table.