Diff view

Free · MIT

Unified or split diff computed from before and after text, with old/new gutters, syntax colors, changed-character highlights inside replaced lines, and Accept or Reject per hunk that collapses the discarded lines.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/diff-view.json

The file lands in components/goodui/diff-view.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/diff-view works too.

Usage

import { DiffView } from "@/components/goodui/diff-view"

<DiffView
  before={original}
  after={proposed}
  language="ts"
  fileName="lib/pricing.ts"
  onDecision={(hunk, decision) => decision && apply(hunk, decision)}
/>

When to use

Use it for
  • Reviewing edits an agent proposes to a file before applying them, hunk by hunk.
  • Showing what changed between two versions of a prompt, config or document.
  • Pull-request style views where syntax colors and changed characters matter.
Reach for something else
  • Diffs of very large files (tens of thousands of lines): compute them server-side and virtualize.
  • Word-level prose comparison: a text diff that highlights words inline reads better than line rows.
If you needUse
Narrow column, chat sidebarmode="unified"
Review on a wide screenmode="split" (old and new side by side)
Accept/Reject buttonsonDecision (or reviewable)
Fewer context linescontext={1}
Long lineswrap

Examples

Controlled decisions

const [decisions, setDecisions] = React.useState<Record<number, "accepted" | "rejected" | undefined>>({})
<DiffView before={a} after={b} decisions={decisions} onDecision={(i, d) => setDecisions((s) => ({ ...s, [i]: d ?? undefined }))} />

Read-only split view

<DiffView before={a} after={b} mode="split" modeToggle={false} language="json" />

API reference

PropTypeDefault
before*string—
after*string—
language"html" | "text" | "ts" | "tsx" | "js" | "jsx" | "json" | "bash" | "sh" | "python" | "py" | "css" | "sql" | "yaml" | "yml""text"
fileNamestring—
mode
unified (one column) or split (old and new side by side).
"unified" | "split"—
defaultMode"unified" | "split""unified"
onModeChange(mode: "unified" | "split") => void—
modeToggle
Show the Unified/Split toggle.
booleantrue
context
Unchanged lines kept around each change.
number3
reviewable
Accept/Reject buttons per hunk. On by default when onDecision is passed.
boolean—
decisions
Hunk decisions by hunk index (controlled).
Record<number, HunkDecision | undefined>—
onDecision(hunkIndex: number, decision: HunkDecision | null, hunk: DiffHunk) => void—
wrap
Wrap long lines instead of scrolling.
booleanfalse
maxHeight
Scroll inside the view beyond this height (px).
number—
labels
Text for built-in controls and screen readers; pass translations here.
Partial<DiffViewLabels>—

Also accepts every <div> attribute.

Accessibility

TabMoves through the view toggle and each hunk's Accept, Reject and Undo buttons.
ArrowLeft / ArrowRightSwitches Unified and Split on the view radio group (native radio behavior).
  • Each hunk is a <section> labelled "Change n"; added and removed lines carry a visually hidden + or − before the text; line numbers and markers are decorative.
  • The +n −n counters in the header are labelled "n additions" and "n deletions". The view toggle is a radiogroup.
  • Decisions show as text badges (Accepted, Rejected) with an Undo button.
  • Built-in strings go through the labels prop for translation.

Motion

  • Accepting a hunk collapses its removed lines (height to 0 in 180 ms) and settles the added ones; rejecting does the reverse. Undo brings them back.
  • The Accept/Reject pair cross-fades with the decision badge; the view toggle's pill glides on a shared-layout spring.
  • Reduced motion: rows and controls swap instantly.
  • The line diff is an LCS over the lines that differ after trimming the common prefix and suffix; pairs larger than 4 million cells fall back to a plain replace. Tokens are computed once per side and reused by both modes.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • Pass whole file contents as before and after; the component computes the diff. Hunks are numbered from 0 in onDecision.
  • decision is "accepted", "rejected" or null (undo). Applying the decision to your file is up to you; the view only reflects it.
  • language uses the code-block tokenizer (ts, tsx, js, json, bash, python, css, html, sql, yaml).
  • Split mode needs about 640px; it scrolls horizontally inside the component when narrower (unless wrap is on).

FAQ

Why are only some characters highlighted inside a changed line?

When a block replaces the same number of lines, each pair is compared and only the differing middle part gets the stronger tint.

Does it produce a patch?

No. Use diffLines and toHunks (exported) to build one from the same data.

Related