Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bscell-card">
<div class="card-body p-3">
<h6 class="fw-bold mb-2">Inventory</h6>
<table class="table table-sm align-middle mb-0">
<thead><tr><th>Item</th><th>Qty</th><th>Unit price</th></tr></thead>
<tbody id="bscellBody"></tbody>
</table>
<p class="small text-muted mt-2 mb-0">Click Qty or Unit price to edit. Enter saves, Escape cancels.</p>
</div>
</div>
</div>.bscell-card { width: 400px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
.bscell-editable { cursor: pointer; border-radius: 5px; padding: 2px 6px; }
.bscell-editable:hover { background: #f0f1f5; }
.bscell-editing input { width: 80px; }let rows = [
{ id: 1, name: 'USB-C Cable', qty: 42, price: 9.99 },
{ id: 2, name: 'Wireless Mouse', qty: 18, price: 24.5 },
{ id: 3, name: 'Laptop Stand', qty: 7, price: 39 },
];
const body = document.getElementById('bscellBody');
function render() {
body.innerHTML = rows.map(r =>
'<tr data-id="' + r.id + '">' +
'<td>' + r.name + '</td>' +
'<td><span class="bscell-editable" data-field="qty">' + r.qty + '</span></td>' +
'<td><span class="bscell-editable" data-field="price">$' + r.price.toFixed(2) + '</span></td>' +
'</tr>'
).join('');
}
function startEdit(cell) {
const td = cell.closest('td');
const tr = cell.closest('tr');
const rowId = Number(tr.dataset.id);
const field = cell.dataset.field;
const row = rows.find(r => r.id === rowId);
const rawValue = field === 'price' ? row.price : row.qty;
td.classList.add('bscell-editing');
td.innerHTML = '<input type="number" class="form-control form-control-sm" value="' + rawValue + '" ' +
(field === 'price' ? 'step="0.01"' : 'step="1"') + '>';
const input = td.querySelector('input');
input.focus();
input.select();
// Removing the input from the DOM (which render() does) itself fires a
// native blur event on it — without this guard, pressing Escape would
// call commit(false), re-render, and then that synthetic blur would
// immediately fire commit(true) again with the same stale input value,
// silently overwriting the discard with an unwanted save.
let committed = false;
function commit(save) {
if (committed) return;
committed = true;
if (save) {
const parsed = Number(input.value);
if (!Number.isNaN(parsed) && parsed >= 0) {
row[field] = field === 'price' ? Math.round(parsed * 100) / 100 : Math.round(parsed);
}
}
render();
}
input.addEventListener('blur', () => commit(true));
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); commit(true); }
if (e.key === 'Escape') { e.preventDefault(); commit(false); }
});
}
body.addEventListener('click', e => {
const cell = e.target.closest('.bscell-editable');
if (cell && !cell.closest('.bscell-editing')) startEdit(cell);
});
render();Bootstrap Inline Table Cell Editing — Free HTML CSS JS Snippet
Bootstrap Inline Table Cell Editing · Tables · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Bootstrap Inline Table Cell Editing — HTML, CSS & JavaScript

Editing happens entirely inside the clicked <td> — startEdit() reads the row's current raw numeric value directly from the rows data array (not by parsing the displayed, already-formatted "$39.00" text back out of the DOM), swaps the cell's content for a real number input pre-filled with that value, and focuses and selects it so typing immediately replaces the old value.
commit(save) is the single function both the blur listener and the keydown handler funnel through — blurring the input (clicking away) and pressing Enter both save, while Escape calls commit(false), which re-renders without touching rows at all, discarding whatever was typed. Centralizing both outcomes in one function is what guarantees "save" always means the same thing regardless of which path triggered it.
A save is only actually applied when the parsed value passes !Number.isNaN(parsed) && parsed >= 0 — typing garbage or a negative number into the cell and pressing Enter silently keeps the row's previous value rather than corrupting it with NaN or a nonsensical negative quantity, and price values are additionally rounded to two decimal places to avoid accumulating floating-point artifacts like 24.500000000000004.
A committed flag guards commit() against running twice for a subtle reason: render() removes the <input> from the DOM, and removing a focused element from the DOM itself fires a native blur event on it. Without the guard, pressing Escape would discard the edit, re-render, and then that synthetic blur would immediately fire commit(true) a second time with the same stale input value — silently overwriting the discard with an unwanted save a moment later.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Hand this snippet to an AI coding assistant like Claude and ask it to add inline validation styling (a red outline) that appears live while an invalid value is typed, before the user even tries to save, or to add support for editing a text field (like the item name) alongside the existing numeric fields, using a text input instead of a number input.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Bootstrap 5.3 table with inline, click-to-edit cells, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.
Requirements:
- A table with at least 3 rows and at least two numeric editable columns (e.g. quantity and price), backed by a plain JavaScript array of row objects holding the real numeric values.
- Clicking an editable cell replaces its content with a real number input pre-filled with that field's actual underlying value (not a value parsed back out of the formatted display text), focused and with its text selected.
- Pressing Enter or blurring the input (clicking elsewhere) must save the edit; pressing Escape must discard it — all three paths should funnel through one shared commit function with a single save/discard branch, not duplicated logic per trigger.
- A save must be rejected (keeping the cell's previous value) if the entered value is not a valid non-negative number. Price values should be rounded to two decimal places on save to avoid floating-point display artifacts.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Load the snippetA three-row inventory table shows, with Qty and Unit price cells styled as clickable.
- 2Click a Qty cellIt becomes a real number input, pre-filled and already selected, ready to type over.
- 3Type a new number and press EnterThe cell saves and reverts to plain text showing the new value.
- 4Click a price cell, type a new value, and click elsewhere on the pageBlurring the input also saves, exactly like pressing Enter does.
- 5Click a cell, type something, then press EscapeThe edit is discarded and the cell reverts to its previous, unchanged value.
- 6Try entering a negative number or clearing the field entirelyThe cell keeps its last valid value rather than accepting invalid input.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Directly from the underlying rows data array (row.qty or row.price), never by parsing the displayed, already-formatted cell text — this avoids re-parsing a "$39.00" string back into a number, which is both unnecessary and error-prone.
The parsed value fails the !Number.isNaN(parsed) && parsed >= 0 check inside commit(), so the save is silently skipped and the cell reverts to its last valid value rather than accepting invalid data.
It saves — the blur event calls commit(true), the same as pressing Enter; only pressing Escape explicitly discards the edit via commit(false).
Yes. Track which cell (row id + field) is currently being edited in component state, conditionally render an input or plain text per cell based on that state, and validate/commit the parsed value the same way on blur, Enter, or Escape.