Diff view
Free · MITUnified 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.
Installation
$ npx shadcn@latest add https://goodui.dev/r/diff-view.jsonThe 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
- 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.
- 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 need | Use |
|---|---|
| Narrow column, chat sidebar | mode="unified" |
| Review on a wide screen | mode="split" (old and new side by side) |
| Accept/Reject buttons | onDecision (or reviewable) |
| Fewer context lines | context={1} |
| Long lines | wrap |
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
| Prop | Type | Default |
|---|---|---|
| before* | string | — |
| after* | string | — |
| language | "html" | "text" | "ts" | "tsx" | "js" | "jsx" | "json" | "bash" | "sh" | "python" | "py" | "css" | "sql" | "yaml" | "yml" | "text" |
| fileName | string | — |
| 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. | boolean | true |
| context Unchanged lines kept around each change. | number | 3 |
| 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. | boolean | false |
| 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
| Tab | Moves through the view toggle and each hunk's Accept, Reject and Undo buttons. |
| ArrowLeft / ArrowRight | Switches 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
Syntax-colored code from a built-in tokenizer or your own tokens, with theme-derived colors, line numbers, highlighted lines, a file tab, copy with a drawn check, a wrap toggle and a staggered line entrance.
Collapsible card for an agent's tool call: name and arguments in the header, a status that morphs from spinner to drawn check or error, a live duration while running, and argument and output panes inside.
Live checklist of an agent's plan: pending, active with a pulsing halo, drawn check, error and skipped states, nested substeps that fold away when done, and a spring progress bar with a step count.
User, assistant and system messages with avatars, grouping, a hover actions row (copy, good/bad, retry, edit), markdown-ish rendering of lists, quotes and fenced code into CodeBlocks, and a blinking caret while the answer streams.