diff --git a/pygitweb/README.md b/pygitweb/README.md
index ea88be4..3e619f6 100644
--- a/pygitweb/README.md
+++ b/pygitweb/README.md
@@ -56,8 +56,9 @@ See [ROUTES.md](ROUTES.md) for more detail.
 - `GET /{project}?a=tags` — list all tags
 - `GET /{project}?a=tag&h=...` — single tag view (tag ref or hash)
 - `GET /{project}?a=commit` — commit information
-- `GET /{project}?a=commitdiff_plain` — unified diff for commit
+- `GET /{project}?a=commitdiff` — commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html))
+- `GET /{project}?a=commitdiff_plain` — unified diff for commit (raw text)
 
 **Project-scoped actions (stub only)**
 
-These actions are accepted but return a minimal placeholder page. Add handlers in `main.py` to implement: `blame`, `blame_incremental`, `blame_data`, `blobdiff`, `blobdiff_plain`, `commitdiff`, `heads`, `patch`, `patches`, `remotes`, `rss`, `atom`, `search`, `search_help`, `snapshot`, `object`.
+These actions are accepted but return a minimal placeholder page. Add handlers in `main.py` to implement: `blame`, `blame_incremental`, `blame_data`, `blobdiff`, `blobdiff_plain`, `heads`, `patch`, `patches`, `remotes`, `rss`, `atom`, `search`, `search_help`, `snapshot`, `object`.
diff --git a/pygitweb/main.py b/pygitweb/main.py
index e55954d..4a1c1be 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -4,6 +4,7 @@ Ported from gitweb/gitweb.perl dispatch and action handlers.
 """
 from __future__ import annotations
 
+import base64
 import mimetypes
 import os
 import subprocess
@@ -505,6 +506,8 @@ def dispatch(
         return git_commit(proj, hash_param)
     if action == "commitdiff_plain":
         return git_commitdiff_plain(proj, hash_param)
+    if action == "commitdiff":
+        return git_commitdiff(proj, hash_param)
     # Stub others with minimal response
     pre = PREAMBLE.render(title=f"{esc_html(action)} - {esc_html(proj)}", theme="dark", site_name=config.SITE_NAME)
     return HTMLResponse(
@@ -948,6 +951,7 @@ def git_commit(project: str, h: str | None) -> HTMLResponse:
     message = (data.get("body") or "").strip()
     commit_link = f"/{project}?a=commit&h={quote(oid, safe='')}"
     tree_link = f"/{project}?a=tree&h={quote(oid, safe='')}"
+    diff_link = f"/{project}?a=commitdiff&h={quote(oid, safe='')}"
     patch_link = f"/{project}?a=commitdiff_plain&h={quote(oid, safe='')}"
     table_rows = [
         ["commit", f'<a href="{commit_link}">{esc_html(oid)}</a>'],
@@ -957,7 +961,7 @@ def git_commit(project: str, h: str | None) -> HTMLResponse:
     ]
     if message:
         table_rows.append(["", f"<pre class='commit-message'>{esc_html(message)}</pre>"])
-    table_rows.append(["", f'<a href="{patch_link}">patch</a>'])
+    table_rows.append(["", f'<a href="{diff_link}">diff</a> · <a href="{patch_link}">patch</a>'])
     table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
     title = f"Commit {esc_html(oid_short)} - {esc_html(project)}"
     pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
@@ -966,8 +970,8 @@ def git_commit(project: str, h: str | None) -> HTMLResponse:
     )
 
 
-def git_commitdiff_plain(project: str, h: str | None) -> PlainTextResponse:
-    """Unified diff for a commit. Uses PyGit2 Diff; returns text/plain unified diff."""
+def _get_commit_unified_diff(project: str, h: str) -> tuple[str, str]:
+    """Return (unified_diff_text, oid_short) for commit h. Raises HTTPException on error."""
     if not h:
         raise HTTPException(status_code=400, detail="Commit hash (h) required")
     if not is_valid_ref_format(h):
@@ -990,6 +994,29 @@ def git_commitdiff_plain(project: str, h: str | None) -> PlainTextResponse:
         if patch.text:
             parts.append(patch.text)
     body = "".join(parts) if parts else ""
+    oid_short = str(commit.id)[:7]
+    return body, oid_short
+
+
+def git_commitdiff(project: str, h: str | None) -> HTMLResponse:
+    """Commit diff page: unified diff rendered with diff2html (script id diff2html-script)."""
+    diff_text, oid_short = _get_commit_unified_diff(project, h or "")
+    # Base64-encode diff so we can embed safely in HTML without breaking script tags
+    diff_b64 = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
+    title = f"Commit diff {oid_short} - {project}"
+    pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
+    body = env.get_template("commitdiff.html").render(
+        project=project,
+        oid_short=oid_short,
+        diff_b64=diff_b64,
+        commit_link=f"/{project}?a=commit&h={quote(h or '', safe='')}",
+    )
+    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+
+
+def git_commitdiff_plain(project: str, h: str | None) -> PlainTextResponse:
+    """Unified diff for a commit. Uses PyGit2 Diff; returns text/plain unified diff."""
+    body, _ = _get_commit_unified_diff(project, h or "")
     return PlainTextResponse(
         body,
         media_type="text/x-diff; charset=utf-8",
diff --git a/pygitweb/static/themes/dark.css b/pygitweb/static/themes/dark.css
index 543df13..db0c092 100644
--- a/pygitweb/static/themes/dark.css
+++ b/pygitweb/static/themes/dark.css
@@ -36,4 +36,12 @@
 	--pgw-rem-fg: #fca5a5;
 	--pgw-rem-bg: #7f1d1d;
 	--pgw-info-fg: #a1a1aa;
+
+	--d2h-info-bg-color: #222;
+	--d2h-file-header-bg-color: rgb(38, 38, 98);
+	--d2h-empty-placeholder-bg-color: #222;
+	--d2h-del-bg-color: rgb(98, 38, 38);
+	--d2h-ins-bg-color: rgb(38, 98, 38);
+	--d2h-dim-color: #b0b0b0;
+	--d2h-bg-color: #222;
 }
diff --git a/pygitweb/templates/commitdiff.html b/pygitweb/templates/commitdiff.html
new file mode 100644
index 0000000..c1c0093
--- /dev/null
+++ b/pygitweb/templates/commitdiff.html
@@ -0,0 +1,57 @@
+<h1>Commit diff {{ oid_short }}</h1>
+<p>Project: <a href="/{{ project }}">{{ project }}</a> · <a href="{{ commit_link }}">commit</a></p>
+<div id="d2h-container" data-diff-b64="{{ diff_b64 }}" data-has-diff="{{ 'true' if diff_b64 else 'false' }}">
+  <div id="d2h-output"></div>
+  <p id="d2h-empty" class="text-muted" style="display: none;">No changes in this commit.</p>
+</div>
+<script>
+(function () {
+  var container = document.getElementById('d2h-container');
+  var output = document.getElementById('d2h-output');
+  var emptyEl = document.getElementById('d2h-empty');
+  var diffB64 = container.getAttribute('data-diff-b64');
+  var hasDiff = container.getAttribute('data-has-diff') === 'true';
+
+  function render() {
+    if (!hasDiff || !diffB64) {
+      emptyEl.style.display = 'block';
+      return;
+    }
+    try {
+      var raw = atob(diffB64);
+      if (!raw) {
+        emptyEl.style.display = 'block';
+        return;
+      }
+      var diffStr = (typeof TextDecoder !== 'undefined')
+        ? new TextDecoder('utf-8').decode(Uint8Array.from(raw, function (c) { return c.charCodeAt(0); }))
+        : raw;
+      if (typeof Diff2HtmlUI === 'undefined') {
+        output.innerHTML = '<p class="text-warning">Diff2HtmlUI not loaded.</p>';
+        return;
+      }
+      var config = {
+        drawFileList: true,
+        matching: 'lines',
+        outputFormat: 'side-by-side',
+        synchronisedScroll: true,
+        highlight: true
+      };
+      var diff2htmlUi = new Diff2HtmlUI(output, diffStr, config);
+      diff2htmlUi.draw();
+      diff2htmlUi.highlightCode();
+    } catch (e) {
+      output.innerHTML = '<p class="text-danger">Failed to render diff: ' + (e.message || e) + '</p>';
+    }
+  }
+
+  var script = document.getElementById('diff2html-script');
+  if (script && (typeof Diff2HtmlUI !== 'undefined')) {
+    render();
+  } else if (script) {
+    script.addEventListener('load', render);
+  } else {
+    render();
+  }
+})();
+</script>
diff --git a/pygitweb/templates/preamble.html b/pygitweb/templates/preamble.html
index a67c2c0..611c0f0 100644
--- a/pygitweb/templates/preamble.html
+++ b/pygitweb/templates/preamble.html
@@ -3,6 +3,8 @@
 <script id="Highlight-script" async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
 <script id="MathJax-script" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-chtml.min.js"></script>
 <script id="GraphViz-script" async src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.8.2/viz.js"></script>
+<script id="diff2html-script" async src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
+<link href=" https://cdn.jsdelivr.net/npm/diff2html@3.4.56/bundles/css/diff2html.min.css " rel="stylesheet">
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Fira+Mono:wght@400;500;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
