Skip to content

Diff View

Diff viewer with unified and side-by-side modes, five visual variants (hairline, bare, dense, viewfinder, statusbar), orthogonal density and palette axes, an opt-in vertical scroll wrapper, and a consumer-filled status-bar slot. Renders as a <figure> with accessible-name resolution via aria-labelledby (figcaption) or aria-label fallback, and exposes keyboard hunk navigation (j/k + Escape).

Info:

mode="split" renders two independently scrolling panes and needs roughly ≥640px of container width to stay readable. On narrow or coarse-pointer viewports prefer mode="unified", which soft-wraps long lines by default there (see wrap) instead of turning the diff into a horizontal filmstrip.

Preview

Installation

$pnpm exec dgadd add ui/diff-view
[Installs to]src/components/ui/diff-view[Item]ui/diff-view

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
import { DiffView } from "@/components/ui/diff-view";const patch = `--- a/src/utils/score.ts+++ b/src/utils/score.ts@@ -1,7 +1,10 @@ import type { Review } from "../types"-export function calculateScore(review: Review): number {-  return review.findings.length * 10+export function calculateScore(review: Review, weights: Record<string, number>): number {+  return review.findings.reduce((total, finding) => {+    const weight = weights[finding.severity] ?? 1+    return total + weight+  }, 0) } export function isPassingScore(score: number): boolean {`;export default function DiffViewDefault() {  return <DiffView patch={patch} />;}

Examples

Minimal

Preview

Hairline

Preview

Bare

Preview

Dense

Preview

Viewfinder

Preview

Status bar

Preview

Palette: Okabe–Ito

Preview

Max height (V-scroll)

Preview

Split mode

Preview

Line numbers

Preview

File stat

Preview

Wrapping long lines

Preview

Before / after compare

Preview

Header caption with a custom accessible name

Preview

API Reference

DiffView

NameTypeDefaultDescription
patchstringUnified diff string.
beforestringOld text. Pair with `after` to let DiffView compute the diff via LCS. The LCS table is capped at 250,000 cells (about 499 lines per side), past which the comparison degrades to a whole-file rewrite; supply `patch` or `diff` for larger inputs.
afterstringNew text. Pair with `before`.
diffParsedDiffPre-parsed diff data, useful when displaying one file out of a multi-file parseDiff() result.
variant"hairline" | "bare" | "dense" | "viewfinder" | "statusbar""hairline"Visual variant. "hairline" (default) is the dashboard-safe bordered look. "bare" removes chrome and renders a 2px left rule; the figcaption is suppressed. "dense" tightens typography and adds visible number-column dividers. "viewfinder" renders four bracketed corners. "statusbar" reveals the bottom statusBar slot.
density"compact" | "default" | "comfortable""default" (or "compact" when variant="dense")Vertical density. Surfaces as data-density on the figure. Orthogonal to variant; variant="dense" defaults this to "compact" unless overridden.
palette"default" | "okabe-ito""default"Color palette for added/removed rows. "okabe-ito" overrides --diff-color-add/--diff-color-remove with a colorblind-safe pair. Surfaces as data-diff-palette on the figure.
mode"unified" | "split""unified"Inline unified view or side-by-side split panes (Old / New).
showLineNumbersbooleantrue (false when variant="bare")Renders line-number gutters. Surfaces as data-line-numbers on the rows container. On by default because every finding in this domain is addressed as file:line; the numbers are user-select: none, so copying a diff still yields clean code.
statbooleantrueRenders the +adds −removes readout at the end of the figcaption. A diff with no add/remove changes renders nothing either way.
wrapbooleanCSS-owned: wraps under 40rem or on coarse pointersSoft-wraps long code lines with a hanging indent instead of scrolling them horizontally. Leave unset to keep the responsive default.
disableWordDiffbooleanfalseDisables intra-line word-level highlighting on added/removed rows.
maxHeightstringCSS length applied to an opt-in vertical scroll wrapper. When set, the rows container gets a fixed max-height and a y-axis scrollbar via the --diff-view-max-h CSS variable.
statusBarReactNodeHeadless bottom slot rendered when variant="statusbar". Fill with whatever your app needs (diff stats, Kbd hints, actions). Omit to suppress the slot entirely.
labelstring"Diff output"Fallback accessible name applied as aria-label when no native ARIA name or figcaption names the figure (variant="bare" or a patch without paths).
regionLabelstring"Unified diff" / "Split diff"Accessible name for the focusable inner rows region. Defaults to "Unified diff" in unified mode and "Split diff" in split mode.
oldSideLabelstring"Old"Accessible name for the split-mode old side group.
newSideLabelstring"New"Accessible name for the split-mode new side group.
emptyLabelstring"No changes"Text for the role="status" empty state when the diff has no hunks.
addedLineLabelstring"Added: "Screen-reader-only prefix announced before each added line.
removedLineLabelstring"Removed: "Screen-reader-only prefix announced before each removed line.
classNamestringAdditional class names merged onto the root <figure>.
refRef<HTMLElement>Ref to the root <figure> element.

Data attributes

AttributeApplies toValuesDescription
data-state[data-row]"added" | "removed" | "context" | "hunk" | "empty"Per-change row state used for row tint, marker color, and hunk styling.
data-wordword-diff spans"added" | "removed"Marks intra-line changed slices for strong-tier word highlighting.
data-variantDiffView"hairline" | "bare" | "dense" | "viewfinder" | "statusbar"Visual chrome variant on the figure.
data-densityDiffView"compact" | "default" | "comfortable"Vertical density axis on the figure.
data-diff-paletteDiffView"default" | "okabe-ito"Palette axis that can override diff add/remove color anchors.
data-modeDiffView"unified" | "split"Rendering mode serialized on the root figure.
data-line-numbersDiffView rows"true" | "false"Whether the rows container renders visible line-number gutters.
data-highlightedhunk rowspresent when activeMarks the currently keyboard-highlighted hunk.
data-wrapDiffView"on" | "off", absent when CSS owns the defaultForces or opts out of soft wrapping for long code lines.
data-emptyDiffView empty bandpresent when the diff has no hunksEnables the hatched empty-state material.
data-max-hDiffViewpresent when maxHeight is setEnables the vertical scroll wrapper styling.

CSS variables

NameDefaultDescription
--diff-view-max-hcomponent-definedCSS length used by the optional vertical scroll wrapper.
--diff-hatchcomponent-definedShared 135° hatch used by both the unmatched split-pane filler and the empty band.
--diff-color-addcomponent-definedDiff-only anchor color for added-line tints.
--diff-color-removecomponent-definedDiff-only anchor color for removed-line tints.
--diff-color-hunkcomponent-definedDiff-only anchor color for hunk-header tints.

Accessibility

Keyboard Navigation

When the rows container is focused, j moves to the next hunk and k moves to the previous one, while Home and End jump to the first and last hunk instead of scrolling the container natively. Navigation does not wrap. Escape clears the active hunk. The active hunk is highlighted with an inset ring and announced via an aria-live region.

KeyAction
jMoves to the next hunk.
kMoves to the previous hunk.
HomeJumps to the first hunk.
EndJumps to the last hunk.
EscapeClears the active hunk highlight.

Notes

Requires @diffgazer/keys (package mode)

DiffView's keyboard hunk navigation (j/k + Escape) imports from the required @diffgazer/keys peer. Package consumers must install @diffgazer/keys with @diffgazer/ui; packages are not yet published to npm, so until the first release install both from locally packed tarballs. Importing @diffgazer/ui/components/diff-view without keys fails at module load with an error naming the missing @diffgazer/keys package. Copy/dgadd consumers do not need the package — copy mode rewrites the keyboard hooks to local source.

Variants

variant="hairline" (default) is the dashboard-grade safe default with a 1px soft border. variant="bare" removes all chrome and renders a 2px left rule that turns accent on hover; the figcaption is suppressed. variant="dense" tightens typography and adds visible dividers between number columns for Gerrit/Reviewable-style review density. variant="viewfinder" renders four bracketed corners for the diff family-consistent inspection language. variant="statusbar" renders a hairline top + content + an optional consumer-filled bottom slot. All variant chrome is driven by [data-variant] selectors in diff-view/diff-view.css.

Inputs

DiffView accepts one of three inputs: a unified diff string (`patch`), before/after raw text (`before`+`after`, computed via LCS), or pre-parsed data (`diff: ParsedDiff`). For multi-file diffs use parseDiff() externally and render one DiffView per file with the `diff` prop.

Accessible Name

Precedence: explicit `aria-labelledby` > explicit `aria-label` > figcaption (when a file label can be derived and variant !== "bare") > `label` prop > "Diff output". An explicit ARIA name leaves the visible figcaption in place but suppresses its generated aria-labelledby. variant="bare" suppresses the figcaption, so `label` becomes the figure name unless native ARIA props override it.

Line States

Each change row exposes a semantic row state on a data-row span. Row tints, marker color, and the strong-tier word-diff overlay derive from public data attributes and diff-only CSS variables, so no inline styles are emitted.

Density and Palette

density ("compact" | "default" | "comfortable") and palette ("default" | "okabe-ito") are orthogonal axes that compose with any variant. variant="dense" defaults density to "compact"; pass an explicit `density` to override. palette="okabe-ito" swaps to a colorblind-safe pair via --diff-color-add/--diff-color-remove overrides on the figure.

Word Diff

Intra-line word highlighting is enabled by default — changed slices on add/remove rows are wrapped in <span data-word="added|removed"> and tinted with the strong-tier overlay. Disable per-line word annotation with `disableWordDiff`.

Wrapping

Long code lines soft-wrap with a 2ch hanging indent instead of scrolling sideways: the gutter and marker stay pinned to the first visual line, and the row tint paints the full wrapped height. The default is owned by CSS — containers under 40rem and coarse pointers wrap, wider pointer-fine containers keep the filmstrip — so a phone gets wrapping without the consumer knowing the prop exists. Pass `wrap` or `wrap={false}` to force either state; the prop surfaces as data-wrap="on" / "off" and its absence is what hands the decision to CSS. Wrapping is pure geometry: no colors, no motion, and copy still yields the original line because text-indent is not copied.

File Stat

The figcaption ends with a `+adds −removes` readout parsed from the hunks, matching how the product writes change size everywhere else. It is on by default (`stat={false}` opts out) and renders nothing for a diff with no add/remove changes. At any width the PATH truncates and the stat does not: an ellipsised path is honest, "+7 −" is a lie. A visually-hidden sibling carries "N additions, N deletions" so the figure's accessible name reads as prose instead of punctuation.

Empty State

A diff with no hunks renders its role="status" band on the same 135° hatch an unmatched split pane uses, with the label knocked out on top — one material for absence across the whole component. variant="bare" keeps the plain label, and forced-colors falls back to a dashed GrayText border.

Vertical Scroll

Pass a CSS length to `maxHeight` to opt into a vertical scroll wrapper around the rows container. The figure sets the --diff-view-max-h CSS variable and `data-max-h`; the shared CSS pins the scroll wrapper height and renders a thin scrollbar. Horizontal scroll still works per-row independently.

Status Bar Slot

variant="statusbar" reveals a headless bottom slot the consumer fills via the `statusBar` prop. The primitive deliberately ships no hard-coded kbd hints, no stats summary, and no copy — render whatever your app needs (diff stats, Kbd hints, action buttons). When `statusBar` is omitted the slot is not rendered at all.

Standalone Utilities

The pure diff functions (parseDiff, computeDiff, resolveDiffInput) and types (ParsedDiff, DiffHunk, DiffChange, ChangeType, DiffInput) are re-exported from the same module so consumers can compose with the primitive or use the diff utilities standalone.

Source

Install via CLI: pnpm exec dgadd add ui/diff-view. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.