UI Components Hard
Data Table
Feature-rich data table with column sorting, row selection via checkboxes, search filter, column visibility toggle, and pagination.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--card2: #0a0f1a;
--border: rgba(255, 255, 255, 0.08);
--text: #f2f6ff;
--muted: #475569;
--accent: #38bdf8;
--row-hover: rgba(255, 255, 255, 0.025);
--row-alt: rgba(255, 255, 255, 0.012);
--sorted-col: rgba(56, 189, 248, 0.04);
--danger: #f87171;
--success: #4ade80;
--warning: #fbbf24;
--radius: 12px;
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
padding: 2rem 1rem;
}
.page {
max-width: 1040px;
margin: 0 auto;
}
/* โโ Table shell โโ */
.table-shell {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
/* โโ Toolbar โโ */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1.125rem;
border-bottom: 1px solid var(--border);
gap: 1rem;
flex-wrap: wrap;
}
.toolbar-left {
flex: 1;
min-width: 180px;
}
.search-wrap {
position: relative;
max-width: 300px;
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
font-size: 1.1rem;
pointer-events: none;
}
.search-input {
width: 100%;
background: var(--card2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
padding: 0.45rem 0.75rem 0.45rem 2.25rem;
font-size: 0.855rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.12);
}
.search-input::placeholder {
color: var(--muted);
}
/* Column toggle button */
.col-toggle-wrap {
position: relative;
}
.icon-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--card2);
border: 1px solid var(--border);
color: var(--muted);
padding: 0.45rem 0.875rem;
border-radius: 8px;
font-size: 0.82rem;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.icon-btn:hover {
color: var(--text);
border-color: rgba(255, 255, 255, 0.18);
}
.col-dropdown {
position: absolute;
right: 0;
top: calc(100% + 6px);
background: #111827;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.4rem;
min-width: 160px;
z-index: 100;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
}
.col-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.45rem 0.6rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.835rem;
color: var(--text);
transition: background 0.15s;
}
.col-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.col-item input[type="checkbox"] {
accent-color: var(--accent);
width: 14px;
height: 14px;
cursor: pointer;
}
/* โโ Bulk bar โโ */
.bulk-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 1.125rem;
background: rgba(56, 189, 248, 0.06);
border-bottom: 1px solid rgba(56, 189, 248, 0.15);
animation: slide-down 0.18s ease;
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.bulk-count {
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
}
.bulk-actions {
display: flex;
gap: 0.5rem;
}
.bulk-btn {
padding: 0.35rem 0.875rem;
border-radius: 6px;
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.05);
color: var(--text);
transition: background 0.2s, border-color 0.2s;
}
.bulk-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.bulk-btn--danger {
color: var(--danger);
border-color: rgba(248, 113, 113, 0.3);
}
.bulk-btn--danger:hover {
background: rgba(248, 113, 113, 0.1);
}
/* โโ Table wrap โโ */
.table-wrap {
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.table-wrap::-webkit-scrollbar {
height: 5px;
}
.table-wrap::-webkit-scrollbar-track {
background: transparent;
}
.table-wrap::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
/* โโ Data table โโ */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.855rem;
}
/* Sticky header */
.data-table thead {
position: sticky;
top: 0;
z-index: 10;
background: var(--card2);
}
.data-table th {
padding: 0.625rem 0.875rem;
text-align: left;
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
border-bottom: 1px solid var(--border);
white-space: nowrap;
user-select: none;
}
.data-table th.sortable {
cursor: pointer;
transition: color 0.2s;
}
.data-table th.sortable:hover {
color: var(--text);
}
.th-inner {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.sort-icon {
font-size: 0.7rem;
color: var(--muted);
transition: opacity 0.2s;
}
.sort-icon--active {
color: var(--accent);
}
/* Sorted column highlight */
.data-table td.col-sorted,
.data-table th.col-sorted {
background: var(--sorted-col);
}
/* โโ Rows โโ */
.data-table tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
.data-table tbody tr:nth-child(even) {
background: var(--row-alt);
}
.data-table tbody tr:hover {
background: var(--row-hover);
}
.data-table tbody tr.selected {
background: rgba(56, 189, 248, 0.06);
}
.data-table td {
padding: 0.75rem 0.875rem;
color: var(--text);
vertical-align: middle;
}
/* Checkbox column */
.col-check {
width: 40px;
}
.data-table input[type="checkbox"] {
accent-color: var(--accent);
width: 15px;
height: 15px;
cursor: pointer;
}
/* โโ Name cell โโ */
.user-name {
font-weight: 500;
}
/* โโ Email cell โโ */
.user-email {
color: var(--muted);
font-size: 0.82rem;
}
/* โโ Role badge โโ */
.role-badge {
display: inline-block;
padding: 0.2em 0.6em;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.06);
color: var(--text);
}
/* โโ Status badges โโ */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2em 0.6em;
border-radius: 999px;
font-size: 0.73rem;
font-weight: 600;
}
.status-badge::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
}
.status-badge--active {
background: rgba(74, 222, 128, 0.1);
color: var(--success);
}
.status-badge--active::before {
background: var(--success);
}
.status-badge--inactive {
background: rgba(71, 85, 105, 0.18);
color: var(--muted);
}
.status-badge--inactive::before {
background: var(--muted);
}
.status-badge--pending {
background: rgba(251, 191, 36, 0.1);
color: var(--warning);
}
.status-badge--pending::before {
background: var(--warning);
}
/* โโ Actions โโ */
.actions-cell {
display: flex;
gap: 0.375rem;
align-items: center;
}
.action-btn {
width: 28px;
height: 28px;
border-radius: 6px;
background: none;
border: 1px solid transparent;
color: var(--muted);
cursor: pointer;
display: grid;
place-items: center;
font-size: 0.9rem;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.06);
border-color: var(--border);
color: var(--text);
}
.action-btn--delete:hover {
background: rgba(248, 113, 113, 0.1);
border-color: rgba(248, 113, 113, 0.3);
color: var(--danger);
}
/* โโ Empty state โโ */
.empty-row td {
text-align: center;
padding: 3rem;
color: var(--muted);
font-size: 0.875rem;
}
/* โโ Pagination โโ */
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.125rem;
border-top: 1px solid var(--border);
flex-wrap: wrap;
gap: 0.5rem;
}
.pagination-info {
font-size: 0.8rem;
color: var(--muted);
}
.page-btns {
display: flex;
gap: 0.25rem;
align-items: center;
}
.page-btn {
min-width: 30px;
height: 30px;
border-radius: 6px;
background: none;
border: 1px solid var(--border);
color: var(--muted);
font-size: 0.82rem;
cursor: pointer;
display: grid;
place-items: center;
padding: 0 0.25rem;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.page-btn:hover:not([disabled]) {
background: rgba(255, 255, 255, 0.06);
color: var(--text);
border-color: rgba(255, 255, 255, 0.18);
}
.page-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #050910;
font-weight: 700;
}
.page-btn[disabled] {
opacity: 0.35;
cursor: not-allowed;
}
.page-ellipsis {
color: var(--muted);
font-size: 0.8rem;
padding: 0 0.25rem;
}(function () {
"use strict";
// โโ Data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const DATA = [
{
id: 1,
name: "Alex Morgan",
email: "alex.morgan@acme.io",
role: "Admin",
status: "active",
joined: "2023-01-15",
},
{
id: 2,
name: "Jordan Lee",
email: "jordan.lee@acme.io",
role: "Editor",
status: "active",
joined: "2023-03-08",
},
{
id: 3,
name: "Sam Rivera",
email: "sam.rivera@acme.io",
role: "Viewer",
status: "inactive",
joined: "2023-05-21",
},
{
id: 4,
name: "Casey Kim",
email: "casey.kim@acme.io",
role: "Editor",
status: "pending",
joined: "2023-07-04",
},
{
id: 5,
name: "Morgan Blake",
email: "morgan.blake@acme.io",
role: "Viewer",
status: "active",
joined: "2023-08-19",
},
{
id: 6,
name: "Taylor Quinn",
email: "taylor.quinn@acme.io",
role: "Admin",
status: "active",
joined: "2023-09-30",
},
{
id: 7,
name: "Drew Hayes",
email: "drew.hayes@acme.io",
role: "Viewer",
status: "inactive",
joined: "2023-11-01",
},
{
id: 8,
name: "Jamie Patel",
email: "jamie.patel@acme.io",
role: "Editor",
status: "active",
joined: "2024-01-22",
},
{
id: 9,
name: "Robin West",
email: "robin.west@acme.io",
role: "Viewer",
status: "pending",
joined: "2024-02-14",
},
{
id: 10,
name: "Avery Chen",
email: "avery.chen@acme.io",
role: "Editor",
status: "active",
joined: "2024-03-05",
},
{
id: 11,
name: "Dakota Reed",
email: "dakota.reed@acme.io",
role: "Viewer",
status: "inactive",
joined: "2024-04-18",
},
{
id: 12,
name: "Charlie Stone",
email: "charlie.stone@acme.io",
role: "Admin",
status: "active",
joined: "2024-05-30",
},
{
id: 13,
name: "Skyler Ramos",
email: "skyler.ramos@acme.io",
role: "Viewer",
status: "pending",
joined: "2024-07-11",
},
{
id: 14,
name: "Reese Torres",
email: "reese.torres@acme.io",
role: "Editor",
status: "active",
joined: "2024-08-03",
},
{
id: 15,
name: "Finley Cross",
email: "finley.cross@acme.io",
role: "Viewer",
status: "inactive",
joined: "2024-09-27",
},
];
// โโ Columns definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const COLUMNS = [
{ id: "check", label: "", sortable: false, visible: true },
{ id: "name", label: "Name", sortable: true, visible: true },
{ id: "email", label: "Email", sortable: true, visible: true },
{ id: "role", label: "Role", sortable: true, visible: true },
{ id: "status", label: "Status", sortable: true, visible: true },
{ id: "joined", label: "Joined", sortable: true, visible: true },
{ id: "actions", label: "Actions", sortable: false, visible: true },
];
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let filteredData = DATA.slice();
let sortCol = null;
let sortDir = null; // "asc" | "desc" | null
let currentPage = 1;
const PAGE_SIZE = 10;
const selected = new Set();
// โโ DOM refs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const theadRow = document.getElementById("thead-row");
const tbody = document.getElementById("tbody");
const pagination = document.getElementById("pagination");
const bulkBar = document.getElementById("bulk-bar");
const bulkCount = document.getElementById("bulk-count");
const searchInput = document.getElementById("search-input");
const colDropdown = document.getElementById("col-dropdown");
const colBtn = document.getElementById("col-toggle-btn");
const colWrap = document.getElementById("col-toggle-wrap");
// โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function render() {
renderHeader();
renderRows();
renderPagination();
renderBulkBar();
renderColDropdown();
}
// โโ Header โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderHeader() {
theadRow.innerHTML = "";
COLUMNS.forEach(function (col) {
if (!col.visible) return;
const th = document.createElement("th");
th.dataset.col = col.id;
if (col.id === "check") {
th.className = "col-check";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = "select-all";
cb.setAttribute("aria-label", "Select all rows");
const pageIds = pageData().map(function (r) {
return r.id;
});
const selCount = pageIds.filter(function (id) {
return selected.has(id);
}).length;
cb.checked = selCount === pageIds.length && pageIds.length > 0;
cb.indeterminate = selCount > 0 && selCount < pageIds.length;
cb.addEventListener("change", function () {
pageData().forEach(function (row) {
cb.checked ? selected.add(row.id) : selected.delete(row.id);
});
render();
});
th.appendChild(cb);
} else {
if (col.sortable) th.classList.add("sortable");
if (sortCol === col.id) th.classList.add("col-sorted");
const inner = document.createElement("span");
inner.className = "th-inner";
const lbl = document.createElement("span");
lbl.textContent = col.label;
inner.appendChild(lbl);
if (col.sortable) {
const icon = document.createElement("span");
icon.className = "sort-icon" + (sortCol === col.id ? " sort-icon--active" : "");
icon.setAttribute("aria-hidden", "true");
icon.textContent = sortCol === col.id ? (sortDir === "asc" ? "โ" : "โ") : "โ";
inner.appendChild(icon);
th.addEventListener("click", function () {
if (sortCol === col.id) {
sortDir = sortDir === "asc" ? "desc" : sortDir === "desc" ? null : "asc";
if (sortDir === null) sortCol = null;
} else {
sortCol = col.id;
sortDir = "asc";
}
currentPage = 1;
applySort();
render();
});
}
th.appendChild(inner);
}
theadRow.appendChild(th);
});
}
// โโ Rows โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderRows() {
tbody.innerHTML = "";
const rows = pageData();
if (!rows.length) {
const tr = document.createElement("tr");
tr.className = "empty-row";
const td = document.createElement("td");
td.setAttribute(
"colspan",
COLUMNS.filter(function (c) {
return c.visible;
}).length
);
td.textContent = "No users found.";
tr.appendChild(td);
tbody.appendChild(tr);
return;
}
rows.forEach(function (row) {
const tr = document.createElement("tr");
if (selected.has(row.id)) tr.classList.add("selected");
COLUMNS.forEach(function (col) {
if (!col.visible) return;
const td = document.createElement("td");
td.dataset.col = col.id;
if (sortCol === col.id) td.classList.add("col-sorted");
switch (col.id) {
case "check": {
td.className = "col-check";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = selected.has(row.id);
cb.setAttribute("aria-label", "Select " + row.name);
cb.addEventListener("change", function () {
cb.checked ? selected.add(row.id) : selected.delete(row.id);
render();
});
td.appendChild(cb);
break;
}
case "name": {
td.innerHTML = '<span class="user-name">' + esc(row.name) + "</span>";
break;
}
case "email": {
td.innerHTML = '<span class="user-email">' + esc(row.email) + "</span>";
break;
}
case "role": {
td.innerHTML = '<span class="role-badge">' + esc(row.role) + "</span>";
break;
}
case "status": {
const cls = "status-badge--" + row.status;
td.innerHTML = '<span class="status-badge ' + cls + '">' + cap(row.status) + "</span>";
break;
}
case "joined": {
td.textContent = formatDate(row.joined);
break;
}
case "actions": {
td.innerHTML = [
'<div class="actions-cell">',
' <button class="action-btn" aria-label="Edit ' +
esc(row.name) +
'" title="Edit">โ</button>',
' <button class="action-btn action-btn--delete" aria-label="Delete ' +
esc(row.name) +
'" title="Delete">๐</button>',
"</div>",
].join("");
td.querySelector(".action-btn").addEventListener("click", function () {
alert("Edit: " + row.name);
});
td.querySelector(".action-btn--delete").addEventListener("click", function () {
if (confirm("Delete " + row.name + "?")) {
filteredData = filteredData.filter(function (r) {
return r.id !== row.id;
});
selected.delete(row.id);
render();
}
});
break;
}
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
// โโ Pagination โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderPagination() {
const total = filteredData.length;
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const start = (currentPage - 1) * PAGE_SIZE + 1;
const end = Math.min(currentPage * PAGE_SIZE, total);
pagination.innerHTML = "";
const info = document.createElement("span");
info.className = "pagination-info";
info.textContent = total > 0 ? "Showing " + start + "โ" + end + " of " + total : "No results";
pagination.appendChild(info);
const btns = document.createElement("div");
btns.className = "page-btns";
// Prev
const prev = pageBtn("โน", function () {
currentPage--;
render();
});
prev.setAttribute("aria-label", "Previous page");
prev.disabled = currentPage === 1;
btns.appendChild(prev);
// Page numbers with ellipsis
const pages = getPageNumbers(currentPage, totalPages);
pages.forEach(function (p) {
if (p === "โฆ") {
const span = document.createElement("span");
span.className = "page-ellipsis";
span.textContent = "โฆ";
btns.appendChild(span);
} else {
const btn = pageBtn(p, function () {
currentPage = p;
render();
});
if (p === currentPage) btn.classList.add("active");
btn.setAttribute("aria-label", "Page " + p);
btn.setAttribute("aria-current", p === currentPage ? "page" : undefined);
btns.appendChild(btn);
}
});
// Next
const next = pageBtn("โบ", function () {
currentPage++;
render();
});
next.setAttribute("aria-label", "Next page");
next.disabled = currentPage === totalPages;
btns.appendChild(next);
pagination.appendChild(btns);
}
function pageBtn(label, onClick) {
const btn = document.createElement("button");
btn.className = "page-btn";
btn.textContent = label;
btn.addEventListener("click", onClick);
return btn;
}
function getPageNumbers(cur, total) {
if (total <= 7)
return Array.from({ length: total }, function (_, i) {
return i + 1;
});
if (cur <= 4) return [1, 2, 3, 4, 5, "โฆ", total];
if (cur >= total - 3) return [1, "โฆ", total - 4, total - 3, total - 2, total - 1, total];
return [1, "โฆ", cur - 1, cur, cur + 1, "โฆ", total];
}
// โโ Bulk bar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderBulkBar() {
if (selected.size > 0) {
bulkBar.removeAttribute("hidden");
bulkCount.textContent = selected.size + " selected";
} else {
bulkBar.setAttribute("hidden", "");
}
}
// โโ Column visibility dropdown โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderColDropdown() {
colDropdown.innerHTML = "";
COLUMNS.forEach(function (col) {
if (col.id === "check" || col.id === "actions") return; // always visible
const item = document.createElement("label");
item.className = "col-item";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = col.visible;
cb.addEventListener("change", function () {
col.visible = cb.checked;
render();
});
const lbl = document.createElement("span");
lbl.textContent = col.label;
item.appendChild(cb);
item.appendChild(lbl);
colDropdown.appendChild(item);
});
}
// โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function pageData() {
const start = (currentPage - 1) * PAGE_SIZE;
return filteredData.slice(start, start + PAGE_SIZE);
}
function applyFilter(query) {
const q = query.toLowerCase().trim();
filteredData = DATA.filter(function (row) {
return !q || row.name.toLowerCase().includes(q) || row.email.toLowerCase().includes(q);
});
applySort();
}
function applySort() {
if (!sortCol || !sortDir) return;
filteredData = filteredData.slice().sort(function (a, b) {
const av = a[sortCol] || "";
const bv = b[sortCol] || "";
const cmp = av.localeCompare(bv, undefined, { numeric: true });
return sortDir === "asc" ? cmp : -cmp;
});
}
function esc(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function cap(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function formatDate(str) {
const d = new Date(str + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
// โโ Events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Search
searchInput.addEventListener("input", function () {
currentPage = 1;
selected.clear();
applyFilter(searchInput.value);
render();
});
// Bulk delete
document.getElementById("bulk-delete").addEventListener("click", function () {
if (!selected.size) return;
if (!confirm("Delete " + selected.size + " selected user(s)?")) return;
filteredData = filteredData.filter(function (r) {
return !selected.has(r.id);
});
selected.clear();
currentPage = 1;
render();
});
// Bulk export (demo)
document.getElementById("bulk-export").addEventListener("click", function () {
const rows = filteredData.filter(function (r) {
return selected.has(r.id);
});
const csv = ["Name,Email,Role,Status,Joined"]
.concat(
rows.map(function (r) {
return [r.name, r.email, r.role, r.status, r.joined].join(",");
})
)
.join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "users.csv";
a.click();
URL.revokeObjectURL(url);
});
// Column visibility toggle
colBtn.addEventListener("click", function () {
const isHidden = colDropdown.hasAttribute("hidden");
if (isHidden) {
colDropdown.removeAttribute("hidden");
colBtn.setAttribute("aria-expanded", "true");
} else {
colDropdown.setAttribute("hidden", "");
colBtn.setAttribute("aria-expanded", "false");
}
});
document.addEventListener("click", function (e) {
if (!colWrap.contains(e.target)) {
colDropdown.setAttribute("hidden", "");
colBtn.setAttribute("aria-expanded", "false");
}
});
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Data Table</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<div class="table-shell" id="table-shell">
<!-- โโ Toolbar โโ -->
<div class="toolbar">
<div class="toolbar-left">
<div class="search-wrap">
<span class="search-icon" aria-hidden="true">โ</span>
<input
type="search"
id="search-input"
class="search-input"
placeholder="Search usersโฆ"
aria-label="Search users"
/>
</div>
</div>
<div class="toolbar-right">
<!-- Column visibility toggle -->
<div class="col-toggle-wrap" id="col-toggle-wrap">
<button class="icon-btn" id="col-toggle-btn" aria-label="Toggle column visibility" aria-haspopup="true" aria-expanded="false">
<span>Columns</span>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 3.5h10M2 7h10M2 10.5h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</button>
<div class="col-dropdown" id="col-dropdown" hidden role="menu" aria-label="Column visibility">
<!-- populated by JS -->
</div>
</div>
</div>
</div>
<!-- โโ Bulk action bar โโ -->
<div class="bulk-bar" id="bulk-bar" hidden aria-live="polite">
<span class="bulk-count" id="bulk-count">0 selected</span>
<div class="bulk-actions">
<button class="bulk-btn bulk-btn--danger" id="bulk-delete">Delete</button>
<button class="bulk-btn" id="bulk-export">Export CSV</button>
</div>
</div>
<!-- โโ Table โโ -->
<div class="table-wrap" role="region" aria-label="Users table" tabindex="0">
<table class="data-table" id="data-table" aria-rowcount="15">
<thead>
<tr id="thead-row">
<!-- populated by JS -->
</tr>
</thead>
<tbody id="tbody">
<!-- populated by JS -->
</tbody>
</table>
</div>
<!-- โโ Pagination โโ -->
<div class="pagination" id="pagination" aria-label="Table pagination">
<!-- populated by JS -->
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Data Table
A feature-rich data table built entirely with vanilla JS and CSS. Handles 15+ rows with sorting, filtering, selection, column visibility, and pagination.
Features
- Sort โ click any column header to cycle asc โ desc โ none; sort indicator arrow updates
- Search โ real-time filter across Name and Email columns; resets to page 1
- Row selection โ individual checkboxes + select-all header checkbox; indeterminate state when partial
- Bulk actions โ bar appears above table when rows are selected, shows count with Delete and Export buttons
- Column visibility โ dropdown lets you hide/show any column; persists across re-renders
- Pagination โ 10 rows per page; prev/next buttons + numbered page buttons; ellipsis for long page lists
How it works
- A
DATAarray holds all 15 user objects;filteredDatais recomputed on search or sort render()slicesfilteredDatafor the current page and rebuilds the<tbody>rows- Column sort state is stored as
{ col, dir }โdircycles throughasc โ desc โ null - Selected row IDs are stored in a
Set; the select-all checkbox reads the intersection with the current page - Column visibility is a plain object keyed by column id; hidden columns are
display: nonefor both<th>and all corresponding<td>
Status badges
- Active โ green
- Inactive โ gray
- Pending โ amber