Git Diff Stat Summary — Free GitHub-Style Diffstat UI
Git Diff Stat Summary · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="gds-panel">
<div class="gds-head">
<h3>3 files changed</h3>
<span class="gds-total"><b class="gds-add">+142</b> <b class="gds-del">−38</b></span>
</div>
<ul class="gds-files" id="gdsFiles">
<li class="gds-file" data-file="0">
<button type="button" class="gds-file-toggle" aria-expanded="false">
<span class="gds-chevron">›</span>
<span class="gds-fname">src/components/Checkout.tsx</span>
<span class="gds-fstat">+86 −12</span>
<span class="gds-diffbar" aria-hidden="true">
<span class="gds-diffbar-add" style="width:74%"></span><span class="gds-diffbar-del" style="width:26%"></span>
</span>
</button>
<div class="gds-diff" hidden>
<pre><code><span class="gds-line ctx"> const [step, setStep] = useState(0);</span>
<span class="gds-line del">- const total = items.reduce((s, i) => s + i.price, 0);</span>
<span class="gds-line add">+ const subtotal = items.reduce((s, i) => s + i.price, 0);</span>
<span class="gds-line add">+ const total = subtotal + shipping - discount;</span>
<span class="gds-line ctx"> return (</span>
<span class="gds-line add">+ <OrderSummary subtotal={subtotal} total={total} /></span>
<span class="gds-line ctx"> );</span></code></pre>
</div>
</li>
<li class="gds-file" data-file="1">
<button type="button" class="gds-file-toggle" aria-expanded="false">
<span class="gds-chevron">›</span>
<span class="gds-fname">src/utils/pricing.ts</span>
<span class="gds-fstat">+41 −9</span>
<span class="gds-diffbar" aria-hidden="true">
<span class="gds-diffbar-add" style="width:82%"></span><span class="gds-diffbar-del" style="width:18%"></span>
</span>
</button>
<div class="gds-diff" hidden>
<pre><code><span class="gds-line ctx"> export function formatPrice(cents) {</span>
<span class="gds-line del">- return '$' + (cents / 100).toFixed(2);</span>
<span class="gds-line add">+ return (cents / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' });</span>
<span class="gds-line ctx"> }</span></code></pre>
</div>
</li>
<li class="gds-file" data-file="2">
<button type="button" class="gds-file-toggle" aria-expanded="false">
<span class="gds-chevron">›</span>
<span class="gds-fname">tests/checkout.spec.ts</span>
<span class="gds-fstat">+15 −17</span>
<span class="gds-diffbar" aria-hidden="true">
<span class="gds-diffbar-add" style="width:47%"></span><span class="gds-diffbar-del" style="width:53%"></span>
</span>
</button>
<div class="gds-diff" hidden>
<pre><code><span class="gds-line ctx"> it('computes the order total', () => {</span>
<span class="gds-line del">- expect(total(items)).toBe(84.50);</span>
<span class="gds-line add">+ expect(total(items, shipping, discount)).toBe(84.50);</span>
<span class="gds-line ctx"> });</span></code></pre>
</div>
</li>
</ul>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px}
.gds-panel{width:100%;max-width:560px;background:#151b23;border:1px solid #262e3a;border-radius:14px;padding:18px;box-shadow:0 24px 60px rgba(0,0,0,.4)}
.gds-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;padding-bottom:12px;border-bottom:1px solid #232b38}
.gds-head h3{font-size:14.5px;font-weight:800;color:#e7ebf3}
.gds-total{font-family:'SFMono-Regular',Consolas,Menlo,monospace;font-size:13px}
.gds-add{color:#4ade80}
.gds-del{color:#f87171}
.gds-files{list-style:none;display:flex;flex-direction:column;gap:4px}
.gds-file-toggle{width:100%;display:grid;grid-template-columns:16px 1fr auto 90px;align-items:center;gap:10px;background:transparent;border:none;border-radius:8px;padding:9px 8px;cursor:pointer;font-family:inherit;text-align:left}
.gds-file-toggle:hover{background:#1a2129}
.gds-chevron{color:#5b6577;font-size:14px;transition:transform .15s;justify-self:center}
.gds-file-toggle[aria-expanded="true"] .gds-chevron{transform:rotate(90deg)}
.gds-fname{font-family:'SFMono-Regular',Consolas,Menlo,monospace;font-size:12.5px;color:#c3cadb;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gds-fstat{font-family:'SFMono-Regular',Consolas,Menlo,monospace;font-size:11.5px;color:#7c869c;white-space:nowrap}
.gds-diffbar{display:flex;height:8px;border-radius:3px;overflow:hidden;background:#1e2530}
.gds-diffbar-add{background:#4ade80}
.gds-diffbar-del{background:#f87171}
.gds-diff{padding:2px 8px 10px 34px}
.gds-diff pre{margin:0;background:#0e1218;border:1px solid #202836;border-radius:8px;padding:10px 0;overflow-x:auto}
.gds-diff code{font-family:'SFMono-Regular',Consolas,Menlo,monospace;font-size:12px;line-height:1.7}
.gds-line{display:block;padding:0 12px;white-space:pre}
.gds-line.add{background:rgba(74,222,128,.09);color:#a5d6a7}
.gds-line.del{background:rgba(248,113,113,.09);color:#f4a3a3}
.gds-line.ctx{color:#7c869c}var toggles = Array.prototype.slice.call(document.querySelectorAll('.gds-file-toggle'));
toggles.forEach(function (btn) {
btn.addEventListener('click', function () {
var li = btn.closest('.gds-file');
var diff = li.querySelector('.gds-diff');
var isOpen = btn.getAttribute('aria-expanded') === 'true';
// Accordion behavior: close any other open diff before opening this one.
toggles.forEach(function (other) {
if (other === btn) return;
other.setAttribute('aria-expanded', 'false');
other.closest('.gds-file').querySelector('.gds-diff').setAttribute('hidden', '');
});
btn.setAttribute('aria-expanded', String(!isOpen));
if (isOpen) {
diff.setAttribute('hidden', '');
} else {
diff.removeAttribute('hidden');
}
});
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), 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
Git Diff Stat Summary — Diffstat Bars With an Expandable Line-Level Preview

GitHub's pull request file list — a colored bar chip showing each file's insertion/deletion ratio next to its name — communicates change size at a glance without opening a single file. This snippet rebuilds that diffstat pattern, plus a click-to-expand line-level preview, entirely in vanilla HTML/CSS/JS. It pairs naturally with a code diff viewer or code comparison widget in a PR review dashboard.
The diffstat bar is two flex children, not a chart
Each file's bar is a flex container with two children — .gds-diffbar-add and .gds-diffbar-del — each given an inline width percentage representing that file's share of insertions versus deletions. No canvas, no SVG: two colored <span>s inside a display: flex container naturally lay out side by side proportional to their widths, which is the entire visual technique behind GitHub's diffstat.
Totals are a separate, deliberately unlinked number
The header's +142 −38 total is written directly in the markup rather than summed from the per-file stats in JS. In a real implementation you'd compute it server-side from the actual diff and inject both the total and the per-file numbers from the same source — the point of keeping them as plain text here is that this snippet's job is the *display* pattern, not diff computation.
One accordion, not independent toggles
Clicking a file's row expands its line-level diff preview and collapses any other currently-open file — the click handler loops through every toggle button, closing all others, before opening (or closing) the one that was clicked. This keeps the panel compact even with many changed files, rather than letting every diff expand independently and pushing the total height very tall.
Colored diff lines follow git's own convention
Inside the expanded preview, each line carries a .ctx, .add, or .del class — context lines stay neutral, added lines get a green tint with a + prefix, removed lines get a red tint with a - prefix — the same visual language as git diff in a terminal or GitHub's own file view, so it reads as instantly familiar to anyone who has looked at a diff before.
Wiring it to real diff data
Replace the hardcoded file list and diff lines with output from your git provider's API (GitHub's compare API, GitLab's diff API, or a local git diff --numstat and git diff parse) — compute each file's add/delete percentage for the bar width, and split the unified diff into context/add/del lines by their leading +/-/space character.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA three-file diff summary renders with colored insertion/deletion bars.
- 2Click a file rowIts line-level diff preview expands below it, with added/removed lines colored and prefixed.
- 3Click another fileThe previous diff collapses automatically — only one stays open at a time.
- 4Feed in real dataReplace the file list and diff lines with parsed output from git diff --numstat and git diff.
- 5Compute bar widthsSet each file's add/delete bar width as its share of that file's total changed lines.
- 6Sum the header totalCompute +142 −38 from the sum of every file's insertions and deletions.
Real-world uses
Common Use Cases
aria-expanded and the hidden attribute kept in sync for accessible disclosure.Got questions?
Frequently Asked Questions
Each bar is a flex container with two child spans — one for insertions, one for deletions — each given an inline-style width percentage representing that file's insertion/deletion ratio. Because they sit inside a display: flex container, the two colored segments naturally lay out side by side proportional to their widths, with no canvas, SVG, or chart dependency involved.
The click handler loops through every toggle button and forces aria-expanded to false (and hides the diff) on every file except the one just clicked, before toggling that one. This single-open accordion pattern keeps the panel's total height manageable even when many files changed, rather than letting every expanded diff stack up.
Run git diff --numstat to get each file's insertion and deletion counts, then compute addPercent = insertions / (insertions + deletions) * 100 and delPercent = 100 - addPercent for that file's bar. Set those as the inline width styles on the two bar segments. The header total is simply the sum of insertions and deletions across all files.
Split the diff text by newline. Any line starting with + (but not +++) is an added line, any line starting with - (but not ---) is a removed line, and any other line is context — apply the matching .add, .del, or .ctx class to each line's element. Strip or keep the leading +/- character depending on whether you want it shown as part of the line text.
Hold an openFileIndex (or null) in component state instead of toggling DOM attributes directly. Render each file's bar widths and diff lines from your parsed diff data, and derive each toggle's aria-expanded and each diff panel's visibility from whether its index matches openFileIndex — clicking a file sets openFileIndex to its own index (or null if already open).