UI Components Medium
Code Diff Viewer
Side-by-side code diff viewer with added/removed line highlighting, line numbers, and unified/split toggle. No libraries.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
min-height: 100vh;
padding: 32px 16px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 900px;
}
.diff-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #21262d;
border-radius: 10px 10px 0 0;
border: 1px solid #30363d;
border-bottom: none;
}
.diff-file {
font-size: 13px;
font-weight: 600;
color: #e6edf3;
font-family: Menlo, monospace;
}
.diff-stats {
display: flex;
gap: 10px;
margin-left: 12px;
}
.stat-add {
font-size: 12px;
font-weight: 700;
color: #3fb950;
}
.stat-del {
font-size: 12px;
font-weight: 700;
color: #f85149;
}
.diff-meta {
display: flex;
align-items: center;
}
.view-toggle {
display: flex;
background: #30363d;
border-radius: 8px;
padding: 2px;
gap: 2px;
}
.view-btn {
background: none;
border: none;
color: #8b949e;
font-size: 12px;
font-weight: 600;
padding: 4px 12px;
border-radius: 6px;
cursor: pointer;
}
.view-btn.active {
background: #484f58;
color: #e6edf3;
}
/* Diff container */
.diff-container {
display: flex;
border: 1px solid #30363d;
border-radius: 0 0 10px 10px;
overflow: hidden;
background: #0d1117;
}
.diff-container--unified {
display: block;
}
.diff-container--unified .diff-line-num {
min-width: 36px;
}
.diff-pane {
flex: 1;
min-width: 0;
overflow-x: auto;
}
.diff-pane-header {
padding: 8px 16px;
font-size: 11px;
font-weight: 700;
color: #8b949e;
background: #161b22;
border-bottom: 1px solid #21262d;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.diff-divider {
width: 1px;
background: #21262d;
}
.diff-lines {
font-family: "JetBrains Mono", Menlo, monospace;
font-size: 12.5px;
line-height: 1.6;
}
/* Lines */
.diff-line {
display: flex;
align-items: stretch;
min-height: 24px;
}
.diff-line:hover {
background: rgba(255, 255, 255, 0.03);
}
.diff-line-num {
min-width: 44px;
padding: 0 8px;
text-align: right;
color: #4a555f;
border-right: 1px solid #21262d;
user-select: none;
font-size: 11px;
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}
.diff-line-sig {
width: 20px;
text-align: center;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.diff-line-text {
flex: 1;
padding: 2px 12px 2px 4px;
color: #e6edf3;
white-space: pre;
}
/* States */
.diff-line--add {
background: rgba(63, 185, 80, 0.1);
}
.diff-line--add .diff-line-sig {
color: #3fb950;
}
.diff-line--del {
background: rgba(248, 81, 73, 0.1);
}
.diff-line--del .diff-line-sig {
color: #f85149;
}
.diff-line--empty {
background: #0d1117;
}
.diff-line--empty .diff-line-text {
color: transparent;
}
.diff-line--hunk {
background: rgba(130, 180, 255, 0.06);
}
.diff-line--hunk .diff-line-text {
color: #79c0ff;
font-style: italic;
}// Diff data: each item is [type, leftNum, rightNum, text]
// type: 'ctx' | 'add' | 'del' | 'hunk'
const DIFF = [
["hunk", null, null, "@@ -1,12 +1,16 @@"],
["ctx", 1, 1, 'import { SignJWT, jwtVerify } from "jose";'],
["ctx", 2, 2, 'import { cookies } from "next/headers";'],
["ctx", 3, 3, ""],
["del", 4, null, "const SECRET = process.env.SECRET;"],
["add", null, 4, "const SECRET = new TextEncoder().encode("],
["add", null, 5, ' process.env.SECRET ?? ""'],
["add", null, 6, ");"],
["ctx", 5, 7, ""],
["del", 6, null, "export async function createToken(payload) {"],
["add", null, 8, "export async function createToken("],
["add", null, 9, " payload: Record<string, unknown>"],
["add", null, 10, ") {"],
["ctx", 7, 11, " return new SignJWT(payload)"],
["ctx", 8, 12, ' .setProtectedHeader({ alg: "HS256" })'],
["ctx", 9, 13, ' .setExpirationTime("7d")'],
["del", 10, null, " .sign(Buffer.from(SECRET));"],
["add", null, 14, " .sign(SECRET);"],
["ctx", 11, 15, "}"],
["ctx", 12, 16, ""],
];
function makeLine(type, num, text) {
const sigMap = { add: "+", del: "-", ctx: " ", hunk: "@@" };
const line = document.createElement("div");
line.className = "diff-line" + (type !== "ctx" ? ` diff-line--${type}` : "");
line.innerHTML = `
<span class="diff-line-num">${num ?? ""}</span>
<span class="diff-line-sig">${sigMap[type] ?? ""}</span>
<span class="diff-line-text">${escHtml(text)}</span>
`;
return line;
}
function makeEmpty() {
const line = document.createElement("div");
line.className = "diff-line diff-line--empty";
line.innerHTML = `<span class="diff-line-num"></span><span class="diff-line-sig"></span><span class="diff-line-text">~</span>`;
return line;
}
function escHtml(t) {
return t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function renderSplit() {
const left = document.getElementById("leftPane");
const right = document.getElementById("rightPane");
left.innerHTML = "";
right.innerHTML = "";
for (const [type, ln, rn, text] of DIFF) {
if (type === "hunk") {
left.appendChild(makeLine("hunk", "...", text));
right.appendChild(makeLine("hunk", "...", text));
} else if (type === "del") {
left.appendChild(makeLine("del", ln, text));
right.appendChild(makeEmpty());
} else if (type === "add") {
left.appendChild(makeEmpty());
right.appendChild(makeLine("add", rn, text));
} else {
left.appendChild(makeLine("ctx", ln, text));
right.appendChild(makeLine("ctx", rn, text));
}
}
}
function makeUnifiedLine(type, oldNum, newNum, text) {
const sigMap = { add: "+", del: "-", ctx: " ", hunk: "@@" };
const line = document.createElement("div");
line.className = "diff-line" + (type !== "ctx" ? ` diff-line--${type}` : "");
line.innerHTML = `
<span class="diff-line-num">${oldNum ?? ""}</span>
<span class="diff-line-num">${newNum ?? ""}</span>
<span class="diff-line-sig">${sigMap[type] ?? ""}</span>
<span class="diff-line-text">${escHtml(text)}</span>
`;
return line;
}
function renderUnified() {
const pane = document.getElementById("unifiedPane");
pane.innerHTML = "";
for (const [type, ln, rn, text] of DIFF) {
if (type === "hunk") {
pane.appendChild(makeUnifiedLine("hunk", "...", "...", text));
} else if (type === "del") {
pane.appendChild(makeUnifiedLine("del", ln, "", text));
} else if (type === "add") {
pane.appendChild(makeUnifiedLine("add", "", rn, text));
} else {
pane.appendChild(makeUnifiedLine("ctx", ln, rn, text));
}
}
}
// View toggle
document.querySelectorAll(".view-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".view-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const v = btn.dataset.view;
document.getElementById("splitView").hidden = v !== "split";
document.getElementById("unifiedView").hidden = v !== "unified";
});
});
renderSplit();
renderUnified();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diff Viewer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<div class="diff-toolbar">
<div class="diff-meta">
<span class="diff-file">src/utils/auth.ts</span>
<span class="diff-stats">
<span class="stat-add">+8</span>
<span class="stat-del">-4</span>
</span>
</div>
<div class="view-toggle">
<button class="view-btn active" data-view="split">Split</button>
<button class="view-btn" data-view="unified">Unified</button>
</div>
</div>
<!-- Split view -->
<div class="diff-container" id="splitView">
<div class="diff-pane">
<div class="diff-pane-header">Before</div>
<div class="diff-lines" id="leftPane"></div>
</div>
<div class="diff-divider"></div>
<div class="diff-pane">
<div class="diff-pane-header">After</div>
<div class="diff-lines" id="rightPane"></div>
</div>
</div>
<!-- Unified view -->
<div class="diff-container diff-container--unified" id="unifiedView" hidden>
<div class="diff-lines" id="unifiedPane"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Side-by-side code diff viewer showing added (green) and removed (red) lines with line numbers and change indicators. Includes a toggle between split and unified views. Pure vanilla JS.