cheat sheet

diff2html-cli

Convert unified diffs and git diff output into styled HTML side-by-side or line-by-line diff views from the command line.

#diff#html#cli#toolingupdated 05-04-2026

diff2html-cli — Beautiful HTML Diffs from the Terminal

What it is

diff2html-cli is an npm command-line wrapper around the diff2html JavaScript library that converts unified diff text into styled HTML reports. It reads a diff from stdin or generates one from git diff internally, and outputs a self-contained HTML file with syntax highlighting, side-by-side or line-by-line layout, and a file summary header. The library is maintained as part of the rtfpessoa/diff2html project. The primary alternative is git diff --stat + a browser-based viewer, but diff2html-cli produces a shareable, self-contained HTML artifact with no server needed.

Reach for it when you need a polished diff you can email, attach to a ticket, or post as a CI artifact — anywhere a Markdown code block or a screenshot of git diff would be inadequate. For interactive terminal viewing, delta (Rust, syntax-highlighted pager) is a better fit; for structural source-level diffs (refactor-aware), difft wins. diff2html-cli sits in the niche of "shareable, self-contained, no-server HTML."

Install

bash
# Global install (recommended for CLI use)
npm install -g diff2html-cli

# Local install (project-scoped)
npm install --save-dev diff2html-cli
npx diff2html --version

Output:

code
5.x.x

Verify the binary

bash
which diff2html
diff2html --help | head -5

Output:

text
/usr/local/bin/diff2html
Usage: diff2html [options] -- [extra diff args]

Options:
  -V, --version                        output the version number

Run via npx (no global install)

bash
npx --yes diff2html-cli -i command -s side -o preview -- HEAD~1

Output: (none — browser opens with the rendered diff)

Syntax

diff2html reads a unified diff from stdin and writes HTML to stdout (or a file). When invoked with -s git it calls git diff internally and pipes the result through automatically.

bash
diff2html [OPTIONS] -- [EXTRA GIT DIFF ARGS]

Output: (none — exits 0 on success, writes HTML to stdout or a file)

Essential options

OptionShortDescription
--input stdin-i stdinRead diff from stdin (default)
--input file-i fileRead diff from a file (-f path)
--input command-i commandRead diff from a shell command (-c "cmd")
--output stdout-o stdoutWrite HTML to stdout (default)
--output preview-o previewOpen in the default browser
--output file-o fileWrite to a file (-f path)
--style line-s lineLine-by-line layout (default)
--style side-s sideSide-by-side layout
--no-mergesExclude merge commits
--summary closedShow file summary collapsed
--summary openShow file summary expanded (default)
--summary hiddenHide file summary
--title <text>-tSet the HTML page title
--highlightCodeEnable syntax highlighting
--synchronisedScrollSync scroll between side-by-side panes
--matching linesAlign changed lines in side-by-side view
--matchWordsThreshold <n>Word-level diff threshold (0.0–1.0, default 0.25)
--maxLineSizeInBlockForComparison <n>Skip line-level diff for long lines
--colorScheme githubColor scheme (github or vs)
--version-vPrint version
--help-hShow help

Line-by-line vs side-by-side output

The default line-by-line view stacks the old and new versions vertically, one change per row. Side-by-side (-s side) places the old version on the left pane and the new version on the right pane, making structural differences easier to read at a glance.

bash
# Default: line-by-line
git diff HEAD~1 | diff2html -i stdin -o stdout > report.html

# Side-by-side
git diff HEAD~1 | diff2html -i stdin -s side -o stdout > report.html

Output: (none — HTML written to report.html)

bash
# Open immediately in the browser
git diff HEAD~1 | diff2html -s side -o preview

Output: (none — browser opens with the diff rendered)

Reading from git

When using -i command, diff2html runs git diff internally. You can pass extra git flags after -- to control what gets diffed.

bash
# Diff against the previous commit
diff2html -i command -- HEAD~1

# Diff a specific file
diff2html -i command -- HEAD~1 -- src/auth.js

# Diff two branches
diff2html -i command -- main..feature/login

# Staged changes only
diff2html -i command -- --cached

# Diff with context lines
diff2html -i command -s side -- HEAD~1 -U10

Output: (none — HTML written to stdout or browser)

bash
# Full git log diff for a commit range, written to a file
diff2html -i command -o file -f changes.html -- main..HEAD

Output:

sql
(changes.html created with all commits between main and HEAD)

File output and embedding

Writing to a file produces a self-contained HTML document with inline CSS and JavaScript that can be attached to tickets, emailed, or archived. Use --highlightCode to add language-aware syntax coloring.

bash
# Styled, self-contained report
git diff HEAD~3 | diff2html \
  -i stdin \
  -s side \
  -o file \
  -f diff-report.html \
  --highlightCode \
  --title "Release diff v1.2 → v1.3"

Output:

java
diff-report.html written (self-contained, no external deps)
bash
# Embed as a fragment (stdout, no <html> wrapper)
# diff2html does not have a native fragment mode;
# pipe the output through sed to strip the outer shell if needed.
git diff HEAD~1 | diff2html -i stdin -o stdout | \
  sed -n '/<div id="diff">/,/<\/body>/p' > fragment.html

Output: (none — fragment written to fragment.html)

Syntax highlighting and matching

--highlightCode enables per-language syntax highlighting using highlight.js bundled inside diff2html. --matching lines aligns changed lines within each hunk so that moved code is visually paired across the side-by-side panes.

bash
git diff HEAD~1 | diff2html \
  -s side \
  --highlightCode \
  --matching lines \
  --synchronisedScroll \
  -o preview

Output: (none — browser opens with syntax-highlighted, scrollsync diff)

bash
# Adjust word-matching sensitivity (default 0.25 = 25% words must match)
git diff HEAD~1 | diff2html -s side --matchWordsThreshold 0.5 -o preview

Output: (none — browser opens with stricter word-level alignment)

Color schemes

diff2html-cli ships with github (light) and vs (dark) color schemes. Both produce self-contained HTML; there is no runtime theme toggle — choose one at generation time.

bash
# GitHub light scheme (default)
git diff HEAD~1 | diff2html -s side --colorScheme github -o file -f light.html

# VS dark scheme
git diff HEAD~1 | diff2html -s side --colorScheme vs -o file -f dark.html

Output: (none — HTML files written)

Output formats: HTML, JSON, and JSON-as-data

The CLI's primary output is HTML, but the underlying diff2html library produces a JSON intermediate representation. You can capture that JSON for programmatic post-processing (counting added/removed lines per file, feeding a custom renderer, etc.) by using the library directly rather than the CLI:

bash
# The CLI only emits HTML or HTML-with-stripped-shell.
# For structured JSON, drop down to the library:
npm install diff2html

Output:

text
added 1 package in 1s
javascript
// diff-to-json.js
import { parse } from "diff2html";
import { readFileSync } from "node:fs";

const rawDiff = readFileSync(0, "utf8");  // stdin
const json = parse(rawDiff);              // array of FileDiff objects
console.log(JSON.stringify(json, null, 2));
bash
git diff HEAD~1 | node diff-to-json.js > diff.json
cat diff.json | jq '.[].newName' | head -3

Output:

text
"src/auth.js"
"src/router.js"
"src/index.js"

The JSON shape contains per-file metadata (oldName, newName, isBinary, addedLines, deletedLines) and per-hunk line-by-line entries — useful for building custom dashboards, reviewer notes, or LLM-friendly summaries.

Parsing existing diff files

Any unified diff produced by git, diff -u, hg diff, or a tool that emits the same format can be piped in. There's nothing git-specific about the input handler beyond the optional -i command shortcut.

bash
# Parse a saved patch file
diff2html -i file -f my-patch.diff -s side -o preview

Output: (none — browser opens with the rendered patch)

bash
# Apply a remote PR locally and render
gh pr diff 123 > pr-123.diff
diff2html -i file -f pr-123.diff -s side --highlightCode -o file -f pr-123.html

Output:

text
pr-123.html written
bash
# Patch from `patch -p1`-style workflows
diff -u file.old file.new | diff2html -i stdin -o preview

Output: (none — browser opens with the file comparison)

Programmatic library use (no CLI)

For build pipelines that already run Node, skipping the CLI saves a subprocess and gives you full control over the HTML template.

bash
npm install diff2html diff2html-cli

Output:

text
added 2 packages in 2s
javascript
// render-diff.js
import { html as diffHtml } from "diff2html";
import { execSync } from "node:child_process";
import { writeFileSync } from "node:fs";

const rawDiff = execSync("git diff HEAD~1", { encoding: "utf8" });

const body = diffHtml(rawDiff, {
  drawFileList: true,
  matching: "lines",
  outputFormat: "side-by-side",   // or 'line-by-line'
  renderNothingWhenEmpty: false,
});

const page = `<!doctype html>
<html><head>
  <meta charset="utf-8" />
  <title>Diff report</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
</head><body>${body}</body></html>`;

writeFileSync("custom-diff.html", page);
console.log("custom-diff.html written");

Output:

text
custom-diff.html written

diff2html-cli vs git diff, delta, and difft

Four very different tools that all visualise diffs — the right pick depends on whether you want a terminal pager, a refactor-aware diff, or a shareable artifact.

ToolOutputStrengthWeakness
git diff (built-in)ANSI terminalAlways available; scriptableNo syntax highlight; flat colors
delta (dandavison)ANSI terminalSyntax-highlighted, side-by-side in TTYTerminal only; no shareable output
difft (Wilfred)ANSI terminalTree-aware, understands moved blocksPer-language parsers; slower; not a drop-in for git diff
diff2html-cliSelf-contained HTMLEmail/ticket/CI-attachable; prettyHeavyweight (browser needed); not interactive

A common pattern: use delta for daily local review, then run diff2html-cli only when you need to attach the diff to a ticket or share it with a non-developer.

bash
# Configure delta as git's default pager
git config --global core.pager delta
git config --global interactive.diffFilter "delta --color-only"

Output: (none — exits 0 on success)

Common pitfalls

  1. No diff input → empty report — if git diff produces no output (e.g. working tree is clean), diff2html writes an HTML file with "No changes" — this is expected, not an error.
  2. -i command requires git in $PATH — the tool calls git internally; ensure git is available in the environment where the command runs (relevant in Docker containers).
  3. Binary filesdiff2html cannot render binary file diffs; git outputs Binary files a/… and b/… differ which is passed through as plain text.
  4. Large diffs — very large diffs (thousands of files, tens of thousands of lines) produce multi-megabyte HTML that is slow to open in a browser; use -- <path> to scope the diff to relevant files.
  5. -f without -o file — the -f <filename> flag only takes effect when combined with -o file; without it, -f is silently ignored.
  6. --no-merges only with -i command--no-merges is passed to git log/git diff internally; it has no effect when reading from stdin.
  7. CRLF line endings — diffs generated on Windows with CRLF may render the line-ending change as a noisy whole-file diff. Configure git config core.autocrlf or strip \r before piping in: git diff | tr -d '\r' | diff2html ….
  8. Whitespace-only diffs render as huge changes — pass -w / --ignore-all-space through to git: diff2html -i command -- HEAD~1 -w.
  9. Output path not auto-created-f writes to the exact path you specify. If the parent directory doesn't exist the command fails with ENOENT. Wrap with mkdir -p in scripts.

Integrating with npm scripts

Add diff2html to package.json scripts so team members can generate reports with a consistent invocation.

json
{
  "scripts": {
    "diff:preview": "git diff HEAD~1 | diff2html -s side -o preview",
    "diff:report": "git diff main..HEAD | diff2html -s side --highlightCode -o file -f dist/diff-report.html"
  }
}

Output: (none — JSON config; run with npm run diff:preview)

bash
npm run diff:report

Output:

bash
> diff2html -s side --highlightCode -o file -f dist/diff-report.html

dist/diff-report.html written

Real-world recipes

PR diff report for code review

Generate a side-by-side diff report for a GitHub pull request branch to attach to a review.

bash
git fetch origin feature/login
git diff origin/main...origin/feature/login | diff2html \
  -s side \
  --highlightCode \
  --title "PR: feature/login vs main" \
  -o file \
  -f pr-feature-login.html

Output:

code
pr-feature-login.html written

CI artifact: diff since last tag

Produce a self-contained HTML diff artifact on every release build.

bash
LAST_TAG=$(git describe --tags --abbrev=0 HEAD~1)
git diff "$LAST_TAG"..HEAD | diff2html \
  -s side \
  --highlightCode \
  --title "Changes since $LAST_TAG" \
  -o file \
  -f artifacts/release-diff.html
echo "Diff report: artifacts/release-diff.html"

Output:

arduino
Diff report: artifacts/release-diff.html

Quick preview of staged changes

Before committing, visually inspect what is staged in a side-by-side browser view.

bash
git diff --cached | diff2html -s side -o preview

Output: (none — browser opens immediately)

Diff two arbitrary files

Use the Unix diff command to generate a unified diff, then pipe to diff2html.

bash
diff -u config.old.json config.new.json | diff2html -i stdin -s side -o preview

Output: (none — browser opens with the file comparison)

Blameworthy commit visualizer

Show the diff that a specific commit introduced, with the commit SHA and message in the title.

bash
SHA=$(git rev-parse HEAD~3)
SUBJECT=$(git log -1 --pretty=%s "$SHA")
git show --no-merges "$SHA" | diff2html \
  -i stdin \
  -s side \
  --highlightCode \
  --title "$SHA: $SUBJECT" \
  -o file \
  -f "diffs/$SHA.html"
echo "Wrote diffs/$SHA.html"

Output:

text
Wrote diffs/c93883e.html

Multi-commit batch report

Iterate over the last N commits and produce one HTML file per commit, ready to drop into an index.html listing.

bash
mkdir -p commit-diffs
for SHA in $(git log -10 --no-merges --pretty=%H); do
  git show "$SHA" | diff2html \
    -i stdin -s side --highlightCode \
    --title "$SHA" -o file -f "commit-diffs/$SHA.html"
done
ls commit-diffs/ | head -3

Output:

text
0a1b2c3.html
1d2e3f4.html
2f3a4b5.html

CI workflow (GitHub Actions)

Upload the diff report as a build artifact every time a PR pushes.

yaml
# .github/workflows/diff-report.yml
name: Diff report
on:
  pull_request:
    branches: [main]
jobs:
  diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: npm install -g diff2html-cli
      - name: Generate report
        run: |
          mkdir -p artifacts
          git diff origin/main...HEAD | diff2html \
            -s side --highlightCode \
            --title "PR ${{ github.event.pull_request.number }}" \
            -o file -f artifacts/pr-diff.html
      - uses: actions/upload-artifact@v4
        with:
          name: pr-diff
          path: artifacts/pr-diff.html

Output: (none — workflow file; commits and pushes trigger the workflow)

Server hot-reload of staged changes

Inside a chokidar watcher, regenerate the report each time the index changes. Useful in pre-commit review tools.

javascript
// watch-diff.js
import chokidar from "chokidar";
import { execSync } from "node:child_process";

function rebuild() {
  execSync(
    "git diff --cached | diff2html -s side --highlightCode -o file -f .diff-preview.html",
    { stdio: "inherit" }
  );
  console.log("Updated .diff-preview.html");
}

chokidar.watch(".git/index").on("change", rebuild);
rebuild();

Output:

text
Updated .diff-preview.html

Strip the outer shell for embedding

When embedding the diff inside an existing page (e.g. a Markdown-based PR review tool), use the library and only render the body fragment.

javascript
import { html as diffHtml } from "diff2html";
import { execSync } from "node:child_process";

const fragment = diffHtml(execSync("git diff HEAD~1", { encoding: "utf8" }), {
  outputFormat: "side-by-side",
  drawFileList: false,
});

// `fragment` is just <div class="d2h-wrapper">…</div> — drop it anywhere
console.log(fragment.length, "chars of HTML");

Output:

text
27432 chars of HTML