Bootstrap Diff Viewer — Free HTML CSS JS Snippet
Bootstrap Diff Viewer · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bsdiff-card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-bold mb-0">Changes in v5</h6>
<span class="small text-muted"><span class="text-success fw-bold" id="bsdiffAdded">0</span> added · <span class="text-danger fw-bold" id="bsdiffRemoved">0</span> removed</span>
</div>
<pre class="bsdiff-view mb-0" id="bsdiffView"></pre>
</div>
</div>
</div>.bsdiff-card { width: 440px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
.bsdiff-view {
margin: 0; padding: 10px 12px; background: #f8f9fb; border-radius: 8px;
font: 12.5px/1.7 ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-word;
}
.bsdiff-line { display: block; padding: 1px 6px; border-radius: 3px; }
.bsdiff-add { background: #dcfce7; color: #166534; }
.bsdiff-remove { background: #fee2e2; color: #991b1b; text-decoration: line-through; }
.bsdiff-same { color: #6b7280; }const OLD_LINES = [
'Our platform helps teams ship faster.',
'Get started with a free 14-day trial.',
'No credit card required.',
'Cancel anytime.',
];
const NEW_LINES = [
'Our platform helps teams ship faster, together.',
'Get started with a free 30-day trial.',
'No credit card required.',
'Trusted by over 2,000 companies.',
];
// A minimal line-level LCS diff — good enough for short, line-oriented
// content like this without pulling in a full diff library.
function diffLines(oldLines, newLines) {
const m = oldLines.length, n = newLines.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
const result = [];
let i = 0, j = 0;
while (i < m && j < n) {
if (oldLines[i] === newLines[j]) { result.push({ type: 'same', text: oldLines[i] }); i++; j++; }
else if (dp[i + 1][j] >= dp[i][j + 1]) { result.push({ type: 'remove', text: oldLines[i] }); i++; }
else { result.push({ type: 'add', text: newLines[j] }); j++; }
}
while (i < m) { result.push({ type: 'remove', text: oldLines[i] }); i++; }
while (j < n) { result.push({ type: 'add', text: newLines[j] }); j++; }
return result;
}
function escapeHtml(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
const view = document.getElementById('bsdiffView');
const diff = diffLines(OLD_LINES, NEW_LINES);
const added = diff.filter(d => d.type === 'add').length;
const removed = diff.filter(d => d.type === 'remove').length;
document.getElementById('bsdiffAdded').textContent = added;
document.getElementById('bsdiffRemoved').textContent = removed;
view.innerHTML = diff.map(d => {
const prefix = d.type === 'add' ? '+ ' : d.type === 'remove' ? '- ' : ' ';
const cls = d.type === 'add' ? 'bsdiff-add' : d.type === 'remove' ? 'bsdiff-remove' : 'bsdiff-same';
return '<span class="bsdiff-line ' + cls + '">' + prefix + escapeHtml(d.text) + '</span>';
}).join('');Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Bootstrap Diff Viewer — HTML, CSS & JavaScript

The naive way to "diff" two lists of lines — compare index 0 to index 0, index 1 to index 1, and so on — breaks the instant a single line is inserted or removed near the top, since every line after that point would then appear "changed" even though most of them are actually identical, just shifted. This snippet instead implements a genuine longest-common-subsequence (LCS) algorithm: a dynamic-programming table dp[i][j] records the length of the longest matching subsequence between the remaining old and new lines from each position, built from the bottom right corner upward.
Walking that table forward from i = 0, j = 0 is what turns the LCS lengths into an actual list of same/add/remove operations — a matching line at the current position advances both pointers as same, and at a mismatch, comparing dp[i+1][j] against dp[i][j+1] decides whether the old line was removed or the new line was inserted, always picking whichever choice preserves the longer eventual match. That's the mechanism that correctly identifies "No credit card required." as unchanged even though it sits at a different combination of surrounding added and removed lines in this demo's before/after text.
Every line's text is HTML-escaped via escapeHtml() before being inserted, so diffed content containing <, >, or & — entirely plausible in real document or config content — can never be misinterpreted as markup once rendered.
Step by step
How to Use
- 1Load the snippetA four-line diff renders: unchanged lines in gray, added lines highlighted green, removed lines struck through in red.
- 2Read the count above the diffIt reads the real number of added and removed lines, computed from the same diff result being rendered.
- 3Look at "No credit card required."It renders as unchanged (gray), correctly recognized as identical even though the lines around it changed.
- 4Compare the last line of each version"Cancel anytime." is shown as removed, and "Trusted by over 2,000 companies." as added — genuinely different lines, not a false "changed" pairing.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Comparing purely by position breaks as soon as a single line is inserted or removed anywhere before the end — every subsequent line would then appear mismatched even if it's identical, just shifted by one position. LCS correctly finds the actual longest matching subsequence regardless of where insertions or deletions occur.
Whole lines only — this is a line-level diff suitable for prose, config files, or any line-oriented content; a word-level diff (useful for highlighting a small change within a long paragraph) would need a similar LCS approach applied to words instead of lines.
Yes — escapeHtml() runs on every line's text before it's inserted into the page, so content containing <, >, or & characters renders as literal text rather than being interpreted as markup.
Yes. The diffLines() function is plain, framework-agnostic JavaScript — call it with your old/new line arrays in component state and map the result to JSX/template elements instead of building an HTML string directly.