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).
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.
Installation
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.
The hosted registry is not public yet because r.b4r7.dev does not resolve. Use this source checkout or a local registry preview until the endpoint returns 200.
Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.
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
Examples
Minimal
Hairline
Bare
Dense
Viewfinder
Status bar
Palette: Okabe–Ito
Max height (V-scroll)
Split mode
Line numbers
File stat
Wrapping long lines
Before / after compare
Header caption with a custom accessible name
API Reference
DiffView
Data attributes
CSS variables
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.
| Key | Action |
|---|---|
| j | Moves to the next hunk. |
| k | Moves to the previous hunk. |
| Home | Jumps to the first hunk. |
| End | Jumps to the last hunk. |
| Escape | Clears 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.