Bootstrap 5 CRUD Data Table with Modal Forms
A CRUD employee table with select-all and bulk delete, plus modal forms for adding and editing records and a confirmation dialog before removing them.
<section class="wdw-stage">
<div class="container" style="max-width:1080px">
<div class="wdw-card overflow-hidden">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 p-4">
<div>
<h2 class="h4 wdw-heading mb-0">Manage employees</h2>
<p class="wdw-muted small mb-0" id="dt-count">5 employees</p>
</div>
<div class="d-flex gap-2 w-100 dt-actions">
<button type="button" class="btn btn-outline-danger rounded-pill flex-fill" id="dt-bulk" disabled>Delete selected</button>
<button type="button" class="btn btn-primary rounded-pill flex-fill" id="dt-add">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add employee
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 dt-table">
<thead>
<tr>
<th scope="col" class="ps-3 ps-sm-4" style="width:48px"><input class="form-check-input" type="checkbox" id="dt-all" aria-label="Select all employees"></th>
<th scope="col">Name</th>
<th scope="col" class="d-none d-sm-table-cell">Email</th>
<th scope="col" class="d-none d-md-table-cell">Address</th>
<th scope="col" class="d-none d-md-table-cell">Phone</th>
<th scope="col" class="text-end pe-3 pe-sm-4">Actions</th>
</tr>
</thead>
<tbody id="dt-body"></tbody>
</table>
</div>
<div class="p-4 small wdw-muted border-top" id="dt-empty" hidden>No employees yet. Add the first one.</div>
</div>
</div>
<div class="modal fade" id="dt-modal" tabindex="-1" aria-labelledby="dt-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content border-0 shadow-lg rounded-4" id="dt-form" novalidate>
<div class="modal-header border-0 p-4 pb-2">
<h3 class="modal-title h5 fw-bold" id="dt-modal-title">Add employee</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4 d-grid gap-3">
<div class="form-floating"><input type="text" class="form-control" id="dt-name" placeholder="Name" required><label for="dt-name">Name</label></div>
<div class="form-floating"><input type="email" class="form-control" id="dt-email" placeholder="Email" required><label for="dt-email">Email</label></div>
<div class="form-floating"><input type="text" class="form-control" id="dt-address" placeholder="Address" required><label for="dt-address">Address</label></div>
<div class="form-floating"><input type="tel" class="form-control" id="dt-phone" placeholder="Phone" required><label for="dt-phone">Phone</label></div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4">Save</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="dt-confirm" tabindex="-1" aria-labelledby="dt-confirm-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow-lg rounded-4 text-center p-4">
<h3 class="h5 fw-bold" id="dt-confirm-title">Delete employee?</h3>
<p class="small wdw-muted" id="dt-confirm-text">This action cannot be undone.</p>
<div class="d-flex gap-2 justify-content-center">
<button type="button" class="btn btn-light rounded-pill" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger rounded-pill px-4" id="dt-confirm-ok">Delete</button>
</div>
</div>
</div>
</div>
</section>
/* 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 */
.dt-table { --bs-table-bg: transparent; }
.dt-table thead th { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--bs-secondary-color); font-weight: 700; background: var(--bs-tertiary-bg); border-bottom-width: 1px; padding-block: .85rem; white-space: nowrap; }
.dt-table td { padding-block: .9rem; white-space: nowrap; }
.dt-table tbody tr:last-child td { border-bottom: 0; }
.dt-avatar { width: 2.25rem; height: 2.25rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 700; background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); flex: none; }
.dt-icon-btn { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; background: transparent; border-radius: .75rem; color: var(--bs-secondary-color); }
.dt-icon-btn:hover { background: var(--bs-tertiary-bg); color: var(--bs-emphasis-color); }
.dt-icon-btn.danger:hover { background: var(--bs-danger-bg-subtle); color: var(--bs-danger-text-emphasis); }
.dt-icon-btn:focus-visible { outline: 3px solid var(--bs-focus-ring-color); }
@media (min-width: 576px) { .dt-actions { width: auto !important; } .dt-actions .btn { flex: 0 0 auto; } }
#dt-bulk:disabled { opacity: 1; color: var(--bs-secondary-color); border-color: var(--bs-border-color); background: transparent; }
@media (max-width:575.98px){.dt-table td{white-space:normal;word-break:break-word}.dt-table td:last-child{white-space:nowrap}}
function initDataTable() {
var body = document.getElementById('dt-body');
if (!body) return;
var rows = [
{ id: 1, name: 'Thomas Hardy', email: '[email protected]', address: '89 Chiaroscuro Rd, Portland, USA', phone: '(171) 555-2222' },
{ id: 2, name: 'Dominique Perrier', email: '[email protected]', address: 'Obere Str. 57, Berlin, Germany', phone: '(313) 555-5735' },
{ id: 3, name: 'Maria Anders', email: '[email protected]', address: '25 rue Lauriston, Paris, France', phone: '(503) 555-9931' },
{ id: 4, name: 'Fran Wilson', email: '[email protected]', address: 'C/ Araquil 67, Madrid, Spain', phone: '(204) 619-5731' },
{ id: 5, name: 'Martin Blank', email: '[email protected]', address: 'Via Monte Bianco 34, Turin, Italy', phone: '(480) 631-2097' }
];
var nextId = 6, editId = null, pending = [];
var all = document.getElementById('dt-all');
var bulk = document.getElementById('dt-bulk');
var count = document.getElementById('dt-count');
var empty = document.getElementById('dt-empty');
var form = document.getElementById('dt-form');
var modal = new bootstrap.Modal(document.getElementById('dt-modal'));
var confirmEl = document.getElementById('dt-confirm');
var confirmModal = new bootstrap.Modal(confirmEl);
var fields = ['name', 'email', 'address', 'phone'];
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; });
}
function initials(n) { return n.split(/\s+/).map(function (w) { return w[0]; }).slice(0, 2).join('').toUpperCase(); }
var pencil = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>';
var trash = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 11v5M14 11v5"/></svg>';
function render() {
body.innerHTML = rows.map(function (r) {
return '<tr data-id="' + r.id + '"><td class="ps-3 ps-sm-4"><input class="form-check-input dt-sel" type="checkbox" aria-label="Select ' + esc(r.name) + '"></td>' +
'<td><div class="d-flex align-items-center gap-3"><span class="dt-avatar d-none d-sm-inline-flex" aria-hidden="true">' + esc(initials(r.name)) + '</span><span class="d-flex flex-column"><span class="fw-semibold text-body-emphasis">' + esc(r.name) + '</span><span class="small text-body-secondary d-sm-none">' + esc(r.email) + '</span></span></div></td>' +
'<td class="text-body-secondary d-none d-sm-table-cell">' + esc(r.email) + '</td><td class="text-body-secondary d-none d-md-table-cell">' + esc(r.address) + '</td><td class="text-body-secondary d-none d-md-table-cell">' + esc(r.phone) + '</td>' +
'<td class="text-end pe-2 pe-sm-3 text-nowrap"><button type="button" class="dt-icon-btn" data-act="edit" aria-label="Edit ' + esc(r.name) + '">' + pencil + '</button>' +
'<button type="button" class="dt-icon-btn danger" data-act="delete" aria-label="Delete ' + esc(r.name) + '">' + trash + '</button></td></tr>';
}).join('');
count.textContent = rows.length + (rows.length === 1 ? ' employee' : ' employees');
empty.hidden = rows.length > 0;
syncBulk();
}
function selectedIds() {
return Array.prototype.map.call(body.querySelectorAll('.dt-sel:checked'), function (c) { return +c.closest('tr').dataset.id; });
}
function syncBulk() {
var boxes = body.querySelectorAll('.dt-sel');
var n = selectedIds().length;
bulk.disabled = n === 0;
bulk.textContent = n ? 'Delete selected (' + n + ')' : 'Delete selected';
all.checked = boxes.length > 0 && n === boxes.length;
all.indeterminate = n > 0 && n < boxes.length;
}
function openForm(row) {
editId = row ? row.id : null;
document.getElementById('dt-modal-title').textContent = row ? 'Edit employee' : 'Add employee';
fields.forEach(function (f) { document.getElementById('dt-' + f).value = row ? row[f] : ''; });
form.classList.remove('was-validated');
modal.show();
}
function askDelete(ids) {
pending = ids;
document.getElementById('dt-confirm-title').textContent = ids.length > 1 ? 'Delete ' + ids.length + ' employees?' : 'Delete employee?';
confirmModal.show();
}
document.getElementById('dt-add').addEventListener('click', function () { openForm(null); });
bulk.addEventListener('click', function () { askDelete(selectedIds()); });
all.addEventListener('change', function () {
body.querySelectorAll('.dt-sel').forEach(function (c) { c.checked = all.checked; });
syncBulk();
});
body.addEventListener('change', function (e) { if (e.target.classList.contains('dt-sel')) syncBulk(); });
body.addEventListener('click', function (e) {
var btn = e.target.closest('[data-act]');
if (!btn) return;
var id = +btn.closest('tr').dataset.id;
if (btn.dataset.act === 'edit') openForm(rows.filter(function (r) { return r.id === id; })[0]);
else askDelete([id]);
});
form.addEventListener('submit', function (e) {
e.preventDefault();
form.classList.add('was-validated');
if (!form.checkValidity()) return;
var data = {};
fields.forEach(function (f) { data[f] = document.getElementById('dt-' + f).value.trim(); });
if (editId === null) { data.id = nextId++; rows.push(data); }
else rows = rows.map(function (r) { return r.id === editId ? Object.assign({}, r, data) : r; });
modal.hide();
render();
});
document.getElementById('dt-confirm-ok').addEventListener('click', function () {
rows = rows.filter(function (r) { return pending.indexOf(r.id) === -1; });
confirmModal.hide();
render();
});
render();
}
initDataTable();
<section class="crud-stage">
<div class="crud container">
<div class="crud-card">
<header class="crud-head">
<div>
<h1 class="fs-2xl fw-bold mb-1">Employees</h1>
<p class="fg-secondary mb-0 fs-sm">Manage your team directory. <span id="selCount">0 selected</span></p>
</div>
<div class="crud-actions">
<button type="button" class="btn-subtle theme-danger" id="bulkDelete" data-bs-toggle="dialog" data-bs-target="#deleteDialog" disabled><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg> Delete</button>
<button type="button" class="btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#addDialog"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg> Add employee</button>
</div>
</header>
<div class="table-responsive">
<table class="table table-hover mb-0" id="empTable">
<thead>
<tr>
<th scope="col"><input type="checkbox" class="check" id="selectAll" aria-label="Select all employees"></th>
<th scope="col">Name</th>
<th scope="col" class="d-none md:d-table-cell">Email</th>
<th scope="col" class="d-none xl:d-table-cell">Address</th>
<th scope="col" class="d-none md:d-table-cell">Phone</th>
<th scope="col" class="text-end">Actions</th>
</tr>
</thead>
<tbody id="empBody">
<tr data-name="Thomas Hardy" data-email="[email protected]" data-address="89 Chiaroscuro Rd, Portland, USA" data-phone="(171) 555-2222">
<td><input type="checkbox" class="check row-check" aria-label="Select Thomas Hardy"></td>
<th scope="row" class="fw-normal"><span class="emp"><span class="avatar avatar-subtle emp-av-1" aria-hidden="true">TH</span><span class="emp-text"><span class="fw-semibold emp-name">Thomas Hardy</span><span class="emp-mail fg-secondary fs-sm md:d-none">[email protected]</span></span></span></th>
<td class="d-none md:d-table-cell" data-field="email">[email protected]</td>
<td class="d-none xl:d-table-cell" data-field="address">89 Chiaroscuro Rd, Portland, USA</td>
<td class="d-none md:d-table-cell" data-field="phone">(171) 555-2222</td>
<td class="text-end">
<button type="button" class="btn-text theme-secondary btn-icon" data-bs-toggle="dialog" data-action="edit" data-bs-target="#editDialog" aria-label="Edit Thomas Hardy"><svg width="18" height="18" 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 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button type="button" class="btn-text theme-danger btn-icon" data-bs-toggle="dialog" data-action="delete" data-bs-target="#deleteDialog" aria-label="Delete Thomas Hardy"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg></button>
</td>
</tr>
<tr data-name="Dominique Perrier" data-email="[email protected]" data-address="Obere Str. 57, Berlin, Germany" data-phone="(313) 555-5735">
<td><input type="checkbox" class="check row-check" aria-label="Select Dominique Perrier"></td>
<th scope="row" class="fw-normal"><span class="emp"><span class="avatar avatar-subtle emp-av-2" aria-hidden="true">DP</span><span class="emp-text"><span class="fw-semibold emp-name">Dominique Perrier</span><span class="emp-mail fg-secondary fs-sm md:d-none">[email protected]</span></span></span></th>
<td class="d-none md:d-table-cell" data-field="email">[email protected]</td>
<td class="d-none xl:d-table-cell" data-field="address">Obere Str. 57, Berlin, Germany</td>
<td class="d-none md:d-table-cell" data-field="phone">(313) 555-5735</td>
<td class="text-end">
<button type="button" class="btn-text theme-secondary btn-icon" data-bs-toggle="dialog" data-action="edit" data-bs-target="#editDialog" aria-label="Edit Dominique Perrier"><svg width="18" height="18" 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 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button type="button" class="btn-text theme-danger btn-icon" data-bs-toggle="dialog" data-action="delete" data-bs-target="#deleteDialog" aria-label="Delete Dominique Perrier"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg></button>
</td>
</tr>
<tr data-name="Maria Anders" data-email="[email protected]" data-address="25, rue Lauriston, Paris, France" data-phone="(503) 555-9931">
<td><input type="checkbox" class="check row-check" aria-label="Select Maria Anders"></td>
<th scope="row" class="fw-normal"><span class="emp"><span class="avatar avatar-subtle emp-av-3" aria-hidden="true">MA</span><span class="emp-text"><span class="fw-semibold emp-name">Maria Anders</span><span class="emp-mail fg-secondary fs-sm md:d-none">[email protected]</span></span></span></th>
<td class="d-none md:d-table-cell" data-field="email">[email protected]</td>
<td class="d-none xl:d-table-cell" data-field="address">25, rue Lauriston, Paris, France</td>
<td class="d-none md:d-table-cell" data-field="phone">(503) 555-9931</td>
<td class="text-end">
<button type="button" class="btn-text theme-secondary btn-icon" data-bs-toggle="dialog" data-action="edit" data-bs-target="#editDialog" aria-label="Edit Maria Anders"><svg width="18" height="18" 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 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button type="button" class="btn-text theme-danger btn-icon" data-bs-toggle="dialog" data-action="delete" data-bs-target="#deleteDialog" aria-label="Delete Maria Anders"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg></button>
</td>
</tr>
<tr data-name="Fran Wilson" data-email="[email protected]" data-address="C/ Araquil, 67, Madrid, Spain" data-phone="(204) 619-5731">
<td><input type="checkbox" class="check row-check" aria-label="Select Fran Wilson"></td>
<th scope="row" class="fw-normal"><span class="emp"><span class="avatar avatar-subtle emp-av-4" aria-hidden="true">FW</span><span class="emp-text"><span class="fw-semibold emp-name">Fran Wilson</span><span class="emp-mail fg-secondary fs-sm md:d-none">[email protected]</span></span></span></th>
<td class="d-none md:d-table-cell" data-field="email">[email protected]</td>
<td class="d-none xl:d-table-cell" data-field="address">C/ Araquil, 67, Madrid, Spain</td>
<td class="d-none md:d-table-cell" data-field="phone">(204) 619-5731</td>
<td class="text-end">
<button type="button" class="btn-text theme-secondary btn-icon" data-bs-toggle="dialog" data-action="edit" data-bs-target="#editDialog" aria-label="Edit Fran Wilson"><svg width="18" height="18" 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 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button type="button" class="btn-text theme-danger btn-icon" data-bs-toggle="dialog" data-action="delete" data-bs-target="#deleteDialog" aria-label="Delete Fran Wilson"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg></button>
</td>
</tr>
<tr data-name="Martin Blank" data-email="[email protected]" data-address="Via Monte Bianco 34, Turin, Italy" data-phone="(480) 631-2097">
<td><input type="checkbox" class="check row-check" aria-label="Select Martin Blank"></td>
<th scope="row" class="fw-normal"><span class="emp"><span class="avatar avatar-subtle emp-av-0" aria-hidden="true">MB</span><span class="emp-text"><span class="fw-semibold emp-name">Martin Blank</span><span class="emp-mail fg-secondary fs-sm md:d-none">[email protected]</span></span></span></th>
<td class="d-none md:d-table-cell" data-field="email">[email protected]</td>
<td class="d-none xl:d-table-cell" data-field="address">Via Monte Bianco 34, Turin, Italy</td>
<td class="d-none md:d-table-cell" data-field="phone">(480) 631-2097</td>
<td class="text-end">
<button type="button" class="btn-text theme-secondary btn-icon" data-bs-toggle="dialog" data-action="edit" data-bs-target="#editDialog" aria-label="Edit Martin Blank"><svg width="18" height="18" 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 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button type="button" class="btn-text theme-danger btn-icon" data-bs-toggle="dialog" data-action="delete" data-bs-target="#deleteDialog" aria-label="Delete Martin Blank"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg></button>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="crud-foot">
<span class="fg-secondary fs-sm">Showing <b id="shownCount">5</b> of <b id="totalCount">25</b> entries</span>
<nav aria-label="Employees pages">
<ul class="pagination mb-0">
<li class="page-item disabled"><span class="page-link">Previous</span></li>
<li class="page-item active" aria-current="page"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</footer>
</div>
</div>
<dialog class="dialog" id="addDialog" aria-labelledby="addTitle">
<form method="dialog" id="addForm" novalidate>
<div class="dialog-header"><h2 class="dialog-title" id="addTitle">Add employee</h2><button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button></div>
<div class="dialog-body vstack gap-5">
<div><label class="form-label" for="add-name">Name</label><input class="form-control" id="add-name" name="name" type="text" required></div>
<div><label class="form-label" for="add-email">Email</label><input class="form-control" id="add-email" name="email" type="email" required></div>
<div><label class="form-label" for="add-address">Address</label><textarea class="form-control" id="add-address" name="address" rows="2" required></textarea></div>
<div><label class="form-label" for="add-phone">Phone</label><input class="form-control" id="add-phone" name="phone" type="tel" required></div>
</div>
<div class="dialog-footer"><button type="button" class="btn-subtle theme-secondary" data-bs-dismiss="dialog">Cancel</button><button type="submit" class="btn-solid theme-primary">Add</button></div>
</form>
</dialog>
<dialog class="dialog" id="editDialog" aria-labelledby="editTitle">
<form method="dialog" id="editForm" novalidate>
<div class="dialog-header"><h2 class="dialog-title" id="editTitle">Edit employee</h2><button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button></div>
<div class="dialog-body vstack gap-5">
<div><label class="form-label" for="edit-name">Name</label><input class="form-control" id="edit-name" name="name" type="text" required></div>
<div><label class="form-label" for="edit-email">Email</label><input class="form-control" id="edit-email" name="email" type="email" required></div>
<div><label class="form-label" for="edit-address">Address</label><textarea class="form-control" id="edit-address" name="address" rows="2" required></textarea></div>
<div><label class="form-label" for="edit-phone">Phone</label><input class="form-control" id="edit-phone" name="phone" type="tel" required></div>
</div>
<div class="dialog-footer"><button type="button" class="btn-subtle theme-secondary" data-bs-dismiss="dialog">Cancel</button><button type="submit" class="btn-solid theme-primary">Save changes</button></div>
</form>
</dialog>
<dialog class="dialog dialog-sm" id="deleteDialog" aria-labelledby="delTitle">
<div class="dialog-header"><h2 class="dialog-title" id="delTitle">Delete employee</h2><button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button></div>
<div class="dialog-body"><p class="mb-2" id="delText">Are you sure you want to delete this employee?</p><p class="fg-danger fs-sm mb-0">This action cannot be undone.</p></div>
<div class="dialog-footer"><button type="button" class="btn-subtle theme-secondary" data-bs-dismiss="dialog">Cancel</button><button type="button" class="btn-solid theme-danger" id="confirmDelete" data-bs-dismiss="dialog">Delete</button></div>
</dialog>
</section>
dialog.dialog:not([open]) { display: none; }
.crud-stage { min-height: 100vh; padding: 1.5rem 0; background: var(--bs-bg-2); display: grid; align-items: center; }
.crud-card { background: var(--bs-bg-body); border: 1px solid var(--bs-border-color); border-radius: 1rem; box-shadow: 0 12px 40px -20px rgb(0 0 0 / .25); overflow: hidden; }
.crud-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: space-between; padding: 1.5rem 1.5rem 1.25rem; }
.crud-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.crud-actions .btn-solid, .crud-actions .btn-subtle { display: inline-flex; align-items: center; gap: .5rem; }
.crud-foot { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-top: 1px solid var(--bs-border-color); }
.crud .pagination { flex-wrap: wrap; }
.crud .table { --bs-table-bg: transparent; }
.crud thead th { font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; color: var(--bs-fg-3); background: var(--bs-bg-1); }
.crud .table > :not(caption) > * > * { padding: .75rem 1rem; vertical-align: middle; white-space: nowrap; }
.crud th:first-child, .crud td:first-child { width: 3.5rem; padding-right: 0; }
.crud td .btn-icon { display: inline-flex; }
.emp { display: inline-flex; align-items: center; gap: .75rem; }
.emp-text { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.emp-mail { overflow: hidden; text-overflow: ellipsis; }
.crud .avatar { flex-shrink: 0; --bs-avatar-size: 2.25rem; font-size: .8125rem; font-weight: 600; }
.emp-av-0 { --bs-avatar-bg: oklch(90% .06 250); --bs-avatar-color: oklch(35% .12 250); }
.emp-av-1 { --bs-avatar-bg: oklch(90% .07 150); --bs-avatar-color: oklch(33% .09 150); }
.emp-av-2 { --bs-avatar-bg: oklch(91% .07 60); --bs-avatar-color: oklch(38% .1 60); }
.emp-av-3 { --bs-avatar-bg: oklch(90% .07 330); --bs-avatar-color: oklch(36% .11 330); }
.emp-av-4 { --bs-avatar-bg: oklch(90% .06 200); --bs-avatar-color: oklch(33% .08 200); }
[data-bs-theme="dark"] .emp-av-0 { --bs-avatar-bg: oklch(35% .08 250); --bs-avatar-color: oklch(90% .06 250); }
[data-bs-theme="dark"] .emp-av-1 { --bs-avatar-bg: oklch(33% .07 150); --bs-avatar-color: oklch(90% .07 150); }
[data-bs-theme="dark"] .emp-av-2 { --bs-avatar-bg: oklch(37% .08 60); --bs-avatar-color: oklch(91% .07 60); }
[data-bs-theme="dark"] .emp-av-3 { --bs-avatar-bg: oklch(36% .09 330); --bs-avatar-color: oklch(90% .07 330); }
[data-bs-theme="dark"] .emp-av-4 { --bs-avatar-bg: oklch(33% .07 200); --bs-avatar-color: oklch(90% .06 200); }
@media (width < 768px) {
.crud-head, .crud-foot { padding-inline: 1rem; }
.crud .table > :not(caption) > * > * { padding-inline: .5rem; }
.crud th:first-child, .crud td:first-child { padding-left: 1rem; width: 2.5rem; }
.crud th:last-child, .crud td:last-child { padding-right: .5rem; }
.emp-text { max-width: 8.5rem; }
}
const rowChecks = () => [...document.querySelectorAll('.row-check')];
const initials = (name) => name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join('');
const state = { row: null, rows: [] }; // rows: pending delete targets
function updateCounts() {
const body = document.getElementById('empBody');
const shown = document.getElementById('shownCount');
const total = document.getElementById('totalCount');
if (!body) return;
const n = body.rows.length;
if (shown) shown.textContent = n;
if (total) total.textContent = n + 20; // 20 more employees live on other pages (demo)
}
function initSelection() {
const all = document.getElementById('selectAll');
const count = document.getElementById('selCount');
const bulk = document.getElementById('bulkDelete');
const body = document.getElementById('empBody');
if (!all || !body) return;
const sync = () => {
const rows = rowChecks();
const n = rows.filter((r) => r.checked).length;
all.checked = rows.length > 0 && n === rows.length;
all.indeterminate = n > 0 && n < rows.length;
if (count) count.textContent = `${n} selected`;
if (bulk) bulk.disabled = n === 0;
};
all.addEventListener('change', () => { rowChecks().forEach((r) => { r.checked = all.checked; }); sync(); });
body.addEventListener('change', (e) => { if (e.target.matches('.row-check')) sync(); });
document.addEventListener('crud:change', sync);
sync();
}
function fillRow(tr, data) {
tr.dataset.name = data.name;
tr.dataset.email = data.email;
tr.dataset.address = data.address;
tr.dataset.phone = data.phone;
tr.querySelector('.emp-name').textContent = data.name;
tr.querySelector('.emp-mail').textContent = data.email;
tr.querySelector('[data-field="email"]').textContent = data.email;
tr.querySelector('[data-field="address"]').textContent = data.address;
tr.querySelector('[data-field="phone"]').textContent = data.phone;
tr.querySelector('.avatar').textContent = initials(data.name);
tr.querySelector('.row-check').setAttribute('aria-label', `Select ${data.name}`);
tr.querySelector('[data-action="edit"]').setAttribute('aria-label', `Edit ${data.name}`);
tr.querySelector('[data-action="delete"]').setAttribute('aria-label', `Delete ${data.name}`);
}
const formData = (form) => Object.fromEntries(new FormData(form));
function initTableActions() {
const body = document.getElementById('empBody');
const edit = document.getElementById('editDialog');
const del = document.getElementById('deleteDialog');
const delText = document.getElementById('delText');
const bulk = document.getElementById('bulkDelete');
if (!body || !edit || !del || !delText) return;
body.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const tr = btn.closest('tr');
state.row = tr;
if (btn.dataset.action === 'edit') {
['name', 'email', 'address', 'phone'].forEach((k) => { edit.querySelector(`[name="${k}"]`).value = tr.dataset[k]; });
} else {
state.rows = [tr];
delText.textContent = `Are you sure you want to delete ${tr.dataset.name}?`;
}
});
if (bulk) {
bulk.addEventListener('click', () => {
state.rows = rowChecks().filter((r) => r.checked).map((r) => r.closest('tr'));
const n = state.rows.length;
delText.textContent = `Are you sure you want to delete ${n} selected ${n === 1 ? 'employee' : 'employees'}?`;
});
}
document.getElementById('confirmDelete')?.addEventListener('click', () => {
state.rows.forEach((tr) => tr.remove());
state.rows = [];
updateCounts();
document.dispatchEvent(new Event('crud:change'));
});
document.getElementById('editForm')?.addEventListener('submit', (e) => {
if (!e.target.reportValidity()) { e.preventDefault(); return; }
if (state.row) fillRow(state.row, formData(e.target));
});
}
function initAddForm() {
const form = document.getElementById('addForm');
const body = document.getElementById('empBody');
if (!form || !body) return;
form.addEventListener('submit', (e) => {
if (!form.reportValidity()) { e.preventDefault(); return; }
const tr = body.rows[0]?.cloneNode(true);
if (!tr) return;
tr.querySelector('.row-check').checked = false;
tr.querySelector('.avatar').className = `avatar avatar-subtle emp-av-${body.rows.length % 5}`;
fillRow(tr, formData(form));
body.append(tr);
form.reset();
updateCounts();
document.dispatchEvent(new Event('crud:change'));
});
}
initSelection();
initTableActions();
initAddForm();
Built on Bootstrap 6.0.0-alpha.1 (ES modules, no jQuery). Load Bootstrap through an import map and run the JavaScript as type="module" — the downloaded file is already set up that way. Alpha releases can still change class names.
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.