Bootstrap 5 Table with Add, Edit and Delete Row Actions
An editable team table with add, inline edit and delete actions, plus role badges and an empty state. Edits are validated before they are saved.
<section class="wdw-stage tbl-stage">
<div class="container" style="max-width: 920px;">
<div class="card wdw-card-sm">
<div class="card-body p-3 p-md-4">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h4 wdw-heading mb-1">Team members</h1>
<p class="text-body-secondary small mb-0"><span id="tblCount">3</span> people with access</p>
</div>
<button type="button" class="btn btn-primary rounded-pill px-4" id="tblAdd">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="me-2" aria-hidden="true"><path d="M8 2a.75.75 0 0 1 .75.75v4.5h4.5a.75.75 0 0 1 0 1.5h-4.5v4.5a.75.75 0 0 1-1.5 0v-4.5h-4.5a.75.75 0 0 1 0-1.5h4.5v-4.5A.75.75 0 0 1 8 2z"/></svg>Add member
</button>
</div>
<table class="table align-middle mb-0 tbl">
<thead>
<tr class="text-uppercase">
<th scope="col">Member</th><th scope="col">Role</th><th scope="col">Status</th><th scope="col" class="text-end">Actions</th>
</tr>
</thead>
<tbody id="tblBody"></tbody>
</table>
<div id="tblEmpty" class="text-center py-5 d-none">
<span class="tbl-empty-icon d-inline-flex align-items-center justify-content-center rounded-circle mb-3" aria-hidden="true"><svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/></svg></span>
<h2 class="h6 fw-semibold text-body-emphasis">No members yet</h2>
<p class="text-body-secondary small mb-3">Invite your first teammate to get started.</p>
<button type="button" class="btn btn-primary rounded-pill px-4" data-action="add-empty">Add member</button>
</div>
</div>
</div>
</div>
<div id="tblLive" class="visually-hidden" role="status" aria-live="polite"></div>
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3">
<div id="tblToast" class="toast align-items-center text-bg-dark border-0" role="status" aria-live="polite" aria-atomic="true" data-bs-delay="6000">
<div class="d-flex align-items-center">
<div class="toast-body" id="tblToastMsg">Member removed.</div>
<button type="button" class="btn btn-sm btn-link text-white fw-semibold me-1" id="tblUndo">Undo</button>
<button type="button" class="btn-close btn-close-white me-2" data-bs-dismiss="toast" aria-label="Dismiss"></button>
</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 */
.tbl { --bs-table-bg: transparent; --bs-table-hover-bg: var(--bs-tertiary-bg); }
.tbl thead th { font-size: .75rem; font-weight: 600; letter-spacing: .06em; color: var(--bs-secondary-color); background: transparent; border-bottom-width: 1px; }
.tbl td, .tbl th { padding: .85rem .75rem; }
.tbl tbody tr { transition: background-color .15s; }
.tbl tbody tr:hover { background: var(--bs-tertiary-bg); }
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl .avatar { width: 40px; height: 40px; flex: none; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; font-size: .85rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); }
.tbl .btn-icon { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 50%; background: transparent; color: var(--bs-secondary-color); }
.tbl .btn-icon:hover { background: var(--bs-secondary-bg); color: var(--bs-body-color); }
.tbl .btn-icon.danger:hover { background: var(--bs-danger-bg-subtle); color: var(--bs-danger-text-emphasis); }
.tbl .btn-icon.ok { color: var(--bs-success); }
.tbl .btn-icon:focus-visible { outline: 0; box-shadow: 0 0 0 .2rem rgba(var(--bs-primary-rgb), .4); }
.tbl td .form-control, .tbl td .form-select { min-width: 110px; border-radius: .5rem; }
.tbl-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: .4rem; }
.tbl-empty-icon { width: 64px; height: 64px; background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); }
.tbl tr.is-new { animation: tblIn .4s ease; }
@keyframes tblIn { from { background: var(--bs-primary-bg-subtle); } }
@media (max-width: 767.98px) {
.tbl thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.tbl tbody tr { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .5rem; padding: .75rem; margin-bottom: .75rem; border: 1px solid var(--bs-border-color); border-radius: 1rem; }
.tbl tbody tr:last-child { margin-bottom: 0; }
.tbl td { display: block; border: 0; padding: .1rem 0; }
.tbl td[data-field=member] { flex: 1 1 100%; }
.tbl td.tbl-actions { margin-left: auto; }
}
@media (prefers-reduced-motion: reduce) { .tbl tr.is-new { animation: none; } .tbl tbody tr { transition: none; } }
(function () {
var ICON = {
edit: '<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M12.9 1.6a1.9 1.9 0 0 1 2.7 2.7l-8.4 8.4-3.6.9.9-3.6 8.4-8.4z"/></svg>',
del: '<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M6 1h4l.5 1H14v1.5H2V2h3.5L6 1zm-2 4h8l-.6 9H4.6L4 5z"/></svg>',
x: '<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M3.7 2.6 8 6.9l4.3-4.3 1.1 1.1L9.1 8l4.3 4.3-1.1 1.1L8 9.1l-4.3 4.3-1.1-1.1L6.9 8 2.6 3.7z"/></svg>',
ok: '<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M6.5 11.8 2.7 8l1.1-1.1 2.7 2.7 5.7-5.7 1.1 1.1z"/></svg>'
};
var BADGE = { Admin: 'primary', Editor: 'secondary', Viewer: 'secondary' };
var STATUS = { Active: 'success', Away: 'warning', Invited: 'secondary' };
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function initials(n) { return n.trim().split(/\s+/).slice(0, 2).map(function (w) { return w[0]; }).join('').toUpperCase() || '?'; }
function btn(action, cls, label, icon) {
return '<button type="button" class="btn-icon ' + cls + '" data-action="' + action + '" aria-label="' + esc(label) + '">' + icon + '</button>';
}
function initTable() {
var body = document.getElementById('tblBody');
var addBtn = document.getElementById('tblAdd');
if (!body || !addBtn) return;
var count = document.getElementById('tblCount');
var empty = document.getElementById('tblEmpty');
var live = document.getElementById('tblLive');
var toastEl = document.getElementById('tblToast');
var toast = toastEl && window.bootstrap ? bootstrap.Toast.getOrCreateInstance(toastEl) : null;
var last = null;
var people = [
{ name: 'Maya Fischer', email: '[email protected]', role: 'Admin', status: 'Active', seen: 'Online now' },
{ name: 'Jonas Weber', email: '[email protected]', role: 'Editor', status: 'Away', seen: '2 hours ago' },
{ name: 'Priya Nair', email: '[email protected]', role: 'Viewer', status: 'Invited', seen: 'Pending' }
];
function say(msg) { live.textContent = ''; setTimeout(function () { live.textContent = msg; }, 30); }
function rowHtml(p, editing) {
var role = p.role, st = STATUS[p.status] || 'secondary';
var member = editing
? '<div class="d-flex flex-column gap-1"><input class="form-control form-control-sm" aria-label="Name" value="' + esc(p.name) + '" placeholder="Full name"><input class="form-control form-control-sm" type="email" aria-label="Email" value="' + esc(p.email) + '" placeholder="[email protected]"></div>'
: '<div class="d-flex align-items-center gap-3"><span class="avatar" aria-hidden="true">' + esc(initials(p.name)) + '</span><div class="lh-sm"><div class="fw-semibold text-body-emphasis">' + esc(p.name) + '</div><div class="small text-body-secondary">' + esc(p.email) + '</div></div></div>';
var roleCell = editing
? '<select class="form-select form-select-sm" aria-label="Role">' + Object.keys(BADGE).map(function (r) { return '<option' + (r === role ? ' selected' : '') + '>' + r + '</option>'; }).join('') + '</select>'
: '<span class="badge rounded-pill ' + (role === 'Admin' ? 'bg-primary-subtle text-primary-emphasis' : 'bg-body-secondary text-body-emphasis') + '">' + esc(role) + '</span>';
var statusCell = '<span class="small"><span class="tbl-dot bg-' + st + '"></span><span class="text-body-emphasis">' + esc(p.status) + '</span><span class="text-body-secondary"> · ' + esc(p.seen) + '</span></span>';
var acts = editing
? btn('save', 'ok', 'Save ' + (p.name || 'member'), ICON.ok) + btn('cancel', '', 'Cancel editing', ICON.x)
: btn('edit', '', 'Edit ' + p.name, ICON.edit) + btn('delete', 'danger', 'Delete ' + p.name, ICON.del);
return '<td data-field="member">' + member + '</td><td>' + roleCell + '</td><td>' + statusCell + '</td><td class="text-end text-nowrap tbl-actions">' + acts + '</td>';
}
function render(focusIdx, focusAction) {
body.innerHTML = '';
people.forEach(function (p, i) {
var tr = document.createElement('tr');
tr.dataset.i = i;
if (p._new) tr.className = 'is-new';
tr.innerHTML = rowHtml(p, !!p._editing);
body.appendChild(tr);
});
var n = people.filter(function (p) { return !p._new; }).length;
count.textContent = n;
empty.classList.toggle('d-none', people.length > 0);
body.closest('table').classList.toggle('d-none', people.length === 0);
if (focusIdx != null) {
var t = body.rows[focusIdx];
var f = t && (t.querySelector('input') || t.querySelector('[data-action=' + focusAction + ']'));
if (f) f.focus();
}
}
function startAdd() {
if (people.some(function (p) { return p._editing && p._new; })) return;
people.push({ name: '', email: '', role: 'Viewer', status: 'Invited', seen: 'Pending', _editing: true, _new: true });
render(people.length - 1);
}
function save(i) {
var row = body.rows[i], p = people[i];
var inputs = row.querySelectorAll('input, select');
var name = inputs[0].value.trim();
if (!name) { inputs[0].classList.add('is-invalid'); inputs[0].focus(); return; }
if (!inputs[1].checkValidity()) { inputs[1].classList.add('is-invalid'); inputs[1].focus(); return; }
p.name = name; p.email = inputs[1].value.trim(); p.role = inputs[2].value;
var wasNew = p._new;
delete p._editing; delete p._new;
render(i, 'edit');
say(wasNew ? name + ' added.' : name + ' updated.');
}
function cancel(i) {
var p = people[i];
if (p._new) { people.splice(i, 1); render(null); addBtn.focus(); return; }
delete p._editing;
render(i, 'edit');
}
function remove(i) {
var p = people[i];
last = { person: p, index: i };
people.splice(i, 1);
var msg = p.name + ' removed.';
document.getElementById('tblToastMsg').textContent = msg;
say(msg + ' Undo available.');
if (toast) toast.show();
var next = people.length ? Math.min(i, people.length - 1) : null;
render(next, 'edit');
if (next == null) addBtn.focus();
}
function undo() {
if (!last) return;
people.splice(last.index, 0, last.person);
var idx = last.index, name = last.person.name;
last = null;
if (toast) toast.hide();
render(idx, 'edit');
say(name + ' restored.');
}
addBtn.addEventListener('click', startAdd);
empty.querySelector('[data-action=add-empty]').addEventListener('click', startAdd);
document.getElementById('tblUndo').addEventListener('click', undo);
body.addEventListener('click', function (e) {
var b = e.target.closest('button[data-action]');
if (!b) return;
var row = b.closest('tr'), i = parseInt(row.dataset.i, 10), a = b.dataset.action;
if (a === 'edit') { people[i]._editing = true; render(i); }
else if (a === 'save') save(i);
else if (a === 'cancel') cancel(i);
else if (a === 'delete') remove(i);
});
render();
}
initTable();
})();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.