Bootstrap 5 User Management Data Table
A user management table with avatars, role and status badges, row removal, CSV export and pagination.
<section class="wdw-stage">
<div class="container" style="max-width: 980px">
<div class="wdw-card overflow-hidden">
<div class="p-4 d-flex flex-wrap align-items-center justify-content-between gap-3">
<div>
<span class="wdw-eyebrow">Team</span>
<h1 class="wdw-heading h3 mb-0">User management</h1>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-secondary um-outline rounded-pill" id="umExport">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m0 0l-4-4m4 4l4-4M4 21h16"/></svg>Export CSV</button>
<button type="button" class="btn btn-primary rounded-pill" data-bs-toggle="tooltip" data-bs-title="Invite a teammate">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add user</button>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0 um-table" id="umTable">
<thead><tr><th scope="col" class="ps-4">User</th><th scope="col" class="d-none d-md-table-cell">Created</th><th scope="col" class="d-none d-sm-table-cell">Role</th><th scope="col">Status</th><th scope="col" class="text-end pe-4"><span class="visually-hidden">Actions</span></th></tr></thead>
<tbody>
<tr>
<td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=12" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">Michael Holz</div><div class="small wdw-muted">[email protected]</div></div></div></td>
<td class="text-nowrap wdw-muted d-none d-md-table-cell">04 Oct 2013</td>
<td class="d-none d-sm-table-cell"><span class="badge rounded-pill bg-primary-subtle text-primary-emphasis um-role">Admin</span></td>
<td><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><span class="status-dot bg-success"></span>Active</span></td>
<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" data-bs-toggle="modal" data-bs-target="#umEditModal" aria-label="Edit Michael Holz"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg></button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove Michael Holz"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button></td>
</tr>
<tr>
<td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=47" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">Paula Wilson</div><div class="small wdw-muted">[email protected]</div></div></div></td>
<td class="text-nowrap wdw-muted d-none d-md-table-cell">05 Aug 2014</td>
<td class="d-none d-sm-table-cell"><span class="badge rounded-pill bg-body-secondary text-body-emphasis um-role">Publisher</span></td>
<td><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><span class="status-dot bg-success"></span>Active</span></td>
<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" data-bs-toggle="modal" data-bs-target="#umEditModal" aria-label="Edit Paula Wilson"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg></button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove Paula Wilson"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button></td>
</tr>
<tr>
<td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=33" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">Antonio Moreno</div><div class="small wdw-muted">[email protected]</div></div></div></td>
<td class="text-nowrap wdw-muted d-none d-md-table-cell">11 May 2015</td>
<td class="d-none d-sm-table-cell"><span class="badge rounded-pill bg-body-secondary text-body-emphasis um-role">Publisher</span></td>
<td><span class="badge rounded-pill bg-danger-subtle text-danger-emphasis"><span class="status-dot bg-danger"></span>Suspended</span></td>
<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" data-bs-toggle="modal" data-bs-target="#umEditModal" aria-label="Edit Antonio Moreno"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg></button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove Antonio Moreno"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button></td>
</tr>
<tr>
<td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=5" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">Mary Saveley</div><div class="small wdw-muted">[email protected]</div></div></div></td>
<td class="text-nowrap wdw-muted d-none d-md-table-cell">06 Sep 2016</td>
<td class="d-none d-sm-table-cell"><span class="badge rounded-pill bg-body-secondary text-body-emphasis um-role">Reviewer</span></td>
<td><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><span class="status-dot bg-success"></span>Active</span></td>
<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" data-bs-toggle="modal" data-bs-target="#umEditModal" aria-label="Edit Mary Saveley"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg></button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove Mary Saveley"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button></td>
</tr>
<tr>
<td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=68" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">Martin Sommer</div><div class="small wdw-muted">[email protected]</div></div></div></td>
<td class="text-nowrap wdw-muted d-none d-md-table-cell">12 Aug 2017</td>
<td class="d-none d-sm-table-cell"><span class="badge rounded-pill bg-body-secondary text-body-emphasis um-role">Moderator</span></td>
<td><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis"><span class="status-dot bg-warning"></span>Inactive</span></td>
<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" data-bs-toggle="modal" data-bs-target="#umEditModal" aria-label="Edit Martin Sommer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg></button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove Martin Sommer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button></td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 d-flex flex-wrap align-items-center justify-content-between gap-3 border-top">
<span class="small wdw-muted" id="umCount">Showing 1–5 of 25 users</span>
<nav aria-label="Users pages"><ul class="pagination pagination-sm mb-0 gap-1" id="umPager"></ul></nav>
</div>
</div>
</div>
</section>
<div class="modal fade" id="umEditModal" tabindex="-1" aria-labelledby="umEditTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content border-0 shadow" id="umEditForm">
<div class="modal-header border-0 pb-0">
<h2 class="modal-title h5 wdw-heading" id="umEditTitle">Edit user</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-grid gap-3">
<div><label class="form-label small fw-semibold" for="umEditName">Name</label><input class="form-control" id="umEditName" required></div>
<div><label class="form-label small fw-semibold" for="umEditRole">Role</label>
<select class="form-select" id="umEditRole"><option>Admin</option><option>Publisher</option><option>Reviewer</option><option>Moderator</option></select></div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-secondary um-outline rounded-pill" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill">Save changes</button>
</div>
</form>
</div>
</div>
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3">
<div class="toast align-items-center text-bg-dark border-0" id="umToast" role="status" aria-live="polite" aria-atomic="true">
<div class="d-flex align-items-center">
<div class="toast-body" id="umToastMsg">User removed</div>
<button type="button" class="btn btn-sm btn-link link-light fw-semibold me-2" id="umUndo">Undo</button>
<button type="button" class="btn-close btn-close-white me-2" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.um-table { --bs-table-bg: transparent; }
.um-table thead th { font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--bs-secondary-color); font-weight: 700; background: var(--bs-tertiary-bg); border-bottom: 0; padding-block: .75rem; }
.um-table td { padding-block: .9rem; }
.um-table tbody tr { transition: background-color .15s; }
.um-table tbody tr:hover { background: var(--bs-tertiary-bg); }
.status-dot { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; margin-right: .4rem; vertical-align: .05em; }
.pagination .page-link { min-width: 36px; min-height: 36px; display: inline-flex; align-items: center; justify-content: center; border: 0; }
.wdw-stage > .container { min-width: 0; }
@media (max-width: 575.98px) { .um-table td, .um-table th { padding-inline: .5rem; } .um-table td.ps-4, .um-table th.ps-4 { padding-left: 1rem !important; } .um-table td.pe-4 { padding-right: .5rem !important; } }
.um-role { font-size: .8125rem; padding: .4em .8em; }
.pagination .page-link { background: transparent; color: var(--bs-body-color); }
.pagination .page-item.active .page-link { background: var(--bs-primary); color: #fff; }
[data-bs-theme="dark"] .pagination .page-item.active .page-link { color: #0f1117; }
.pagination .page-item.disabled .page-link { background: transparent; color: var(--bs-secondary-color); opacity: .55; }
.pagination .page-link:not(:disabled):hover { background: var(--bs-tertiary-bg); }
.um-table .user-edit { color: var(--bs-body-color); }
.um-outline { --bs-btn-color: var(--bs-emphasis-color); --bs-btn-border-color: var(--bs-secondary-color); --bs-btn-hover-bg: var(--bs-tertiary-bg); --bs-btn-hover-color: var(--bs-emphasis-color); --bs-btn-hover-border-color: var(--bs-emphasis-color); --bs-btn-active-bg: var(--bs-secondary-bg); --bs-btn-active-color: var(--bs-emphasis-color); }
function initUserTable() {
const table = document.getElementById('umTable');
if (!table) return;
const body = table.querySelector('tbody');
const count = document.getElementById('umCount');
const pager = document.getElementById('umPager');
const per = 5;
const imgs = [12, 47, 33, 5, 68];
const roles = ['Admin', 'Publisher', 'Publisher', 'Reviewer', 'Moderator'];
const statuses = ['Active', 'Active', 'Suspended', 'Active', 'Inactive'];
const names = ['Michael Holz', 'Paula Wilson', 'Antonio Moreno', 'Mary Saveley', 'Martin Sommer', 'Ana Duarte', 'Liam Chen', 'Sofia Rossi', 'Noah Keller', 'Priya Nair', 'Omar Haddad', 'Elena Petrova', 'Jonas Berg', 'Mia Fischer', 'Lucas Silva', 'Hana Sato', 'Ivan Novak', 'Clara Dubois', 'Tom Walker', 'Ruth Adler', 'Karl Lund', 'Nina Costa', 'Felix Moor', 'Zoe Ward', 'Ravi Patel'];
const users = names.map((n, i) => ({
name: n, email: n.split(' ')[0].toLowerCase() + '@acme.io', img: imgs[i % 5] + (i > 4 ? 10 : 0),
created: String(4 + (i * 3) % 24).padStart(2, '0') + ' ' + ['Oct', 'Aug', 'May', 'Sep', 'Aug'][i % 5] + ' ' + (2013 + Math.floor(i / 3)),
role: roles[i % 5], status: statuses[i % 5]
}));
let page = 1;
let last = null;
const statusCls = { Active: 'success', Suspended: 'danger', Inactive: 'warning' };
const icoEdit = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>';
const icoDel = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>';
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const pages = () => Math.max(1, Math.ceil(users.length / per));
const render = () => {
page = Math.min(page, pages());
const slice = users.slice((page - 1) * per, page * per);
body.innerHTML = slice.map((u) => {
const i = users.indexOf(u);
const sc = statusCls[u.status];
const role = u.role === 'Admin' ? 'bg-primary-subtle text-primary-emphasis' : 'bg-body-secondary text-body-emphasis';
return '<tr data-i="' + i + '"><td class="ps-4"><div class="d-flex align-items-center gap-3"><img src="https://i.pravatar.cc/80?img=' + u.img + '" width="40" height="40" class="rounded-circle d-none d-sm-block" alt=""><div><div class="fw-semibold text-body-emphasis">' + esc(u.name) + '</div><div class="small wdw-muted">' + esc(u.email) + '</div></div></div></td>' +
'<td class="text-nowrap wdw-muted d-none d-md-table-cell">' + u.created + '</td>' +
'<td class="d-none d-sm-table-cell"><span class="badge rounded-pill um-role ' + role + '">' + u.role + '</span></td>' +
'<td><span class="badge rounded-pill um-role bg-' + sc + '-subtle text-' + sc + '-emphasis"><span class="status-dot bg-' + sc + '"></span>' + u.status + '</span></td>' +
'<td class="text-end text-nowrap pe-4"><button type="button" class="btn btn-sm btn-outline-secondary border-0 px-2 user-edit" aria-label="Edit ' + esc(u.name) + '">' + icoEdit + '</button><button type="button" class="btn btn-sm btn-outline-danger border-0 px-2 user-remove" aria-label="Remove ' + esc(u.name) + '">' + icoDel + '</button></td></tr>';
}).join('');
const from = users.length ? (page - 1) * per + 1 : 0;
count.textContent = 'Showing ' + from + '–' + (from ? from + slice.length - 1 : 0) + ' of ' + users.length + ' users';
let li = '<li class="page-item' + (page === 1 ? ' disabled' : '') + '"><button type="button" class="page-link rounded-pill px-3" data-go="' + (page - 1) + '"' + (page === 1 ? ' disabled' : '') + '>Previous</button></li>';
for (let p = 1; p <= pages(); p++) {
li += '<li class="page-item' + (p === page ? ' active' : '') + '"' + (p === page ? ' aria-current="page"' : '') + '><button type="button" class="page-link rounded-pill" data-go="' + p + '">' + p + '</button></li>';
}
li += '<li class="page-item' + (page === pages() ? ' disabled' : '') + '"><button type="button" class="page-link rounded-pill px-3" data-go="' + (page + 1) + '"' + (page === pages() ? ' disabled' : '') + '>Next</button></li>';
pager.innerHTML = li;
};
pager.addEventListener('click', (e) => {
const b = e.target.closest('[data-go]');
if (!b || b.disabled) return;
page = +b.dataset.go;
render();
});
const toastEl = document.getElementById('umToast');
const toast = bootstrap.Toast.getOrCreateInstance(toastEl, { delay: 6000 });
body.addEventListener('click', (e) => {
const tr = e.target.closest('tr');
if (!tr) return;
const i = +tr.dataset.i;
if (e.target.closest('.user-remove')) {
last = { u: users[i], i };
users.splice(i, 1);
render();
document.getElementById('umToastMsg').textContent = last.u.name + ' removed';
toast.show();
} else if (e.target.closest('.user-edit')) {
editing = i;
document.getElementById('umEditName').value = users[i].name;
document.getElementById('umEditRole').value = users[i].role;
}
});
document.getElementById('umUndo').addEventListener('click', () => {
if (!last) return;
users.splice(last.i, 0, last.u);
last = null;
render();
toast.hide();
});
let editing = -1;
const modalEl = document.getElementById('umEditModal');
document.getElementById('umEditForm').addEventListener('submit', (e) => {
e.preventDefault();
if (editing < 0) return;
users[editing].name = document.getElementById('umEditName').value.trim() || users[editing].name;
users[editing].role = document.getElementById('umEditRole').value;
users[editing].email = users[editing].name.split(' ')[0].toLowerCase() + '@acme.io';
render();
bootstrap.Modal.getOrCreateInstance(modalEl).hide();
});
document.getElementById('umExport').addEventListener('click', () => {
const lines = users.map((u) => [u.name, u.email, u.created, u.role, u.status].join(','));
const blob = new Blob(['Name,Email,Created,Role,Status\n' + lines.join('\n')], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'users.csv';
a.click();
URL.revokeObjectURL(a.href);
});
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => new bootstrap.Tooltip(el));
render();
}
initUserTable();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.