# Diff Viewer

> A React diff viewer for two texts: split or unified views, word highlights, collapsed context, syntax colors and jumps between changes.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add diff-viewer`
- Docs: https://wingo-ui.com/components/diff-viewer

## Interaction

Read the change at a glance: removed lines are tinted red with a minus, added lines green with a plus, and the words that changed inside a line are marked. Wide screens show the two versions side by side; phones (and the server render) show one unified column, and the switch in the toolbar, or the ⋯ menu on a phone (a bottom sheet), flips between them with a short crossfade. Long runs of unchanged lines fold into a row that says how many lines hide there: tap it to show them all, or use the arrows beside it to reveal 20 at a time; the new rows fade in. The chevron buttons, or Alt+Down and Alt+Up while focus is anywhere inside the viewer, scroll to the next or previous change, focus it and flash it briefly (no flash with reduced motion), wrapping around at the ends; a screen reader hears Change 2 of 5. The ⋯ menu copies either version and toggles line wrapping, ignoring whitespace and ignoring case; the copy buttons in the column headers do the same per side. Lines scroll sideways inside the viewer with the line numbers pinned, the body scrolls with the keyboard once focused, and every changed line carries a spoken added or removed label.

## Installation

```bash
npx wingo-ui@latest add diff-viewer
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/diff-viewer` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/diff-viewer.tsx`

npm dependencies: `npm i diff prism-react-renderer motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useClipboard](https://wingo-ui.com/components/use-clipboard) ([Markdown](https://wingo-ui.com/components/use-clipboard.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { DiffViewer } from "@/components/ui/diff-viewer"

const v3 = `Art. 5. Payment
The Client pays each invoice within 15 days of receiving it.
Payment is made by bank transfer.`

const v4 = `Art. 5. Payment
The Client pays each invoice within 30 calendar days of the issue date,
by bank transfer to account 4839 2017 5 at Chase.`

export function ContractChanges() {
  return (
    <DiffViewer
      oldValue={v3}
      newValue={v4}
      oldTitle="Agreement v3 · Sep 12, 2026"
      newTitle="Agreement v4 · Sep 24, 2026"
      labels={{ split: "Side by side", noChanges: "The versions match" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `oldValue` | `string` | `"# Services agreement no. 118/2026\nThe parties: Acme Software Inc., Chicago, Tax ID 40-1122334, as the Provider,\nand Tech Solutions Inc., Tax ID 12-3456789, as the Client.\n\n## Art. 4. Scope of the agreement\nThe Provider licenses, sets up and maintains the Acme CRM platform\nfor 25 users, as listed in Appendix 1.\nServices are delivered remotely, on business days, between 9:00 AM and 6:00 PM.\n\n## Art. 5. Payment\nMonthly fee: $4,990 plus sales tax\nThe Client pays each invoice within 15 days of receiving it.\nPayment is made by bank transfer or in cash at the Provider's office.\nLate payment accrues a penalty of 0.1% per day of the amount due, capped at the value of the overdue invoice.\n\n## Art. 6. Term\nThe agreement runs for 12 months, starting on the date it is signed.\nThe agreement renews automatically for further 12-month periods unless either party terminates it in writing.\nNotice of termination is given at least 30 days before expiry.\n\n## Art. 7. Confidentiality\nThe parties keep confidential the information received while performing the agreement.\nThe obligation stays in force for 2 years after the agreement ends.\nPublic information and information required by a competent authority are not confidential.\n\n## Art. 8.  Force majeure\nForce majeure releases the party that invokes it from liability, as provided by law.\nThe affected party notifies the other party within 5 days of the event.\n\n## Art. 9. Disputes\nDISPUTES are settled amicably or, failing that, by the courts of Cook County, Illinois.\n\n## Art. 10. Final provisions\nThis agreement was signed in two copies, one for each party.\n"` | The original text. Line endings are normalized (CRLF, LF), text with NUL bytes shows a binary notice, and inputs above 2,000 lines compare in idle time behind a skeleton. |
| `newValue` | `string` | `"# Services agreement no. 118/2026\nThe parties: Acme Software Inc., Chicago, Tax ID 40-1122334, as the Provider,\nand Tech Solutions Inc., Tax ID 12-3456789, as the Client.\n\n## Art. 4. Scope of the agreement\nThe Provider licenses, sets up and maintains the Acme CRM platform\nfor 40 users, as listed in Appendix 1.\nServices are delivered remotely, on business days, between 9:00 AM and 6:00 PM.\n\n## Art. 5. Payment\nMonthly fee: $6,490 plus sales tax\nThe Client pays each invoice within 30 calendar days of the issue date,\nby bank transfer to account 4839 2017 5 (routing 021000021) at Chase.\nLate payment accrues a penalty of 0.1% per day of the amount due, capped at the value of the overdue invoice.\n\n## Art. 6. Term\nThe agreement runs for 12 months, starting on the date it is signed.\nThe agreement renews automatically for further 12-month periods unless either party terminates it in writing.\nNotice of termination is given at least 30 days before expiry.\n\n## Art. 7. Confidentiality\nThe parties keep confidential the information received while performing the agreement.\nThe obligation stays in force for 2 years after the agreement ends.\nPublic information and information required by a competent authority are not confidential.\n\n## Art. 8. Force majeure\nForce majeure releases the party that invokes it from liability, as provided by law.\nThe affected party notifies the other party within 5 days of the event.\n\n## Art. 9. Disputes\nDisputes are settled amicably or, failing that, by the courts of Cook County, Illinois.\n\n## Art. 10. Final provisions\nThis agreement was signed in two copies, one for each party.\nAppendices 1 and 2 are an integral part of the agreement.\n"` | The changed text. |
| `oldTitle?` | `ReactNode` | `"Agreement v3 · Sep 12, 2026"` | The original's name above its column (split) or first in the header (unified). Empty shows labels.original ("Original"). |
| `newTitle?` | `ReactNode` | `"Agreement v4 · Sep 24, 2026"` | The changed version's name. Empty shows labels.modified ("Changed"). |
| `defaultView?` | `"auto" \| "split" \| "unified"` | `"auto"` | The starting view. auto is unified below 768px and on the server, split from 768px. Use view and onViewChange to control it. |
| `view?` | `"auto" \| "split" \| "unified"` |  | Controlled view. |
| `onViewChange?` | `(view: "split" \| "unified") => void` |  | Called when the person switches the view in the toolbar or the ⋯ menu. |
| `granularity?` | `"word" \| "char" \| "none"` | `"word"` | Highlights inside a changed line pair: whole words, single characters, or none. Pairs that share 40% of their text or less get the line tint only. |
| `contextLines?` | `number` | `3` | Unchanged lines kept around each change; longer unchanged runs fold into an expander row. -1 shows every line. |
| `lineNumbers?` | `boolean` | `true` | The old and new line number gutters. They stay pinned while long lines scroll sideways. |
| `wrap?` | `boolean` | `false` | Long lines wrap instead of scrolling sideways. The ⋯ menu toggles it too; a new prop value resets it. |
| `ignoreWhitespace?` | `boolean` | `false` | Lines that differ only in spaces or tabs count as unchanged (and a missing final newline is ignored). Also in the ⋯ menu. |
| `ignoreCase?` | `boolean` | `false` | Lines that differ only in upper and lower case count as unchanged. Also in the ⋯ menu. |
| `language?` | `string` | `""` | A Prism language for syntax colors (tsx, jsx, ts, js, json, yaml, markdown, css, sql, python, go...). Colors come from the tone tokens, so both themes read well. Empty keeps plain text. |
| `showStats?` | `boolean` | `true` | The +12 −3 badges in the toolbar (spoken as 12 additions, 3 deletions). |
| `toolbar?` | `boolean` | `true` | The toolbar with the stats, the view switch, previous / next change and the ⋯ menu. Alt+Up / Alt+Down keep working without it. |
| `size?` | `"sm" \| "md"` | `"md"` | Mono text 12px on 20px lines, or 13px on 22px lines. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius of the panel: 0, 12, 16 or 24px (full falls back to lg). |
| `maxHeight?` | `number` | `560` | Height cap in px; the body scrolls inside it with a sticky header. 0 lets the viewer grow with its content. |
| `onLineClick?` | `(line: DiffLine, side: "old" \| "new") => void` |  | Turns the line numbers into buttons, e.g. to comment on a line. DiffLine is { type: "added" \| "removed" \| "unchanged" \| "collapsed"; oldNumber?; newNumber?; content; parts?; hidden?; noNewline? }. Unified rows with both numbers report the side clicked. |
| `renderGutter?` | `(line: DiffLine, side: "old" \| "new") => ReactNode` |  | An extra cell in each gutter, after the numbers: comment counts, blame avatars, coverage marks. |
| `setApi?` | `(api: DiffViewerApi) => void` |  | Receives { nextChange(), previousChange(), expandAll(), collapseUnchanged() } once after mount; the object keeps its identity. |
| `labels?` | `Partial<DiffViewerLabels>` |  | Every visible and spoken string, English by default (DEFAULT_DIFF_VIEWER_LABELS): split, unified, view, nextChange, previousChange, more, cancel, expand(count), expandUp(count), expandDown(count), added, removed, noChanges, copyOld, copyNew, copied, copyFailed, wrap, ignoreWhitespace, ignoreCase, stats(stats), changes(count), changeOf(index, total), caption(old, new, stats), original, modified, computing, noNewline, lineEndingsOnly, binary, line(n). |
| `classNames?` | `Partial<Record<"toolbar" \| "header" \| "title" \| "body" \| "table" \| "row" \| "number" \| "sign" \| "content" \| "word" \| "collapsed" \| "empty", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the panel, merged last. |

## Parts

### computeDiff (function)

The framework-free diff the viewer uses: { rows: DiffRow[], stats: { added, removed, changed }, blocks, lineEndingsOnly, binary }. Rows are unchanged pairs, change pairs (removed left, added right, with word parts) and collapsed runs. Signature: (oldValue: string, newValue: string, options?: { granularity?, ignoreWhitespace?, ignoreCase?, contextLines? }) => DiffResult.

### DEFAULT_DIFF_VIEWER_LABELS (constant)

Every English string: the Split / Unified switch, Next and Previous change, the expand buttons ("Show 12 unchanged lines"), copy and option menu items, stats ("3 additions, 1 deletion"), changeOf and the screen reader caption. Spread it into labels to translate a few. Type: DiffViewerLabels.

### DiffLine (type)

One line as onLineClick receives it: { type: "added" | "removed" | "unchanged" | "collapsed", oldNumber?, newNumber?, content, parts?, hidden?, noNewline? }.

## Examples

- **Code review**: A tsx change with syntax colors; unchanged runs fold around three lines of context.
- **Config with ignoreWhitespace**: Only the real edits count: trailing spaces and double spaces are ignored.
- **3,000 lines, one change**: Big inputs compare in idle time behind a skeleton, then fold down to the one change.
- **Character highlights**: Unified with granularity char, for short edits like a product title.
- **Line comments**: renderGutter shows comment counts; onLineClick opens a comment box.
- **No differences**: Identical texts show a calm empty state; the file still unfolds on demand.

## See also

- [Code Block](https://wingo-ui.com/components/code-block) ([Markdown](https://wingo-ui.com/components/code-block.md))
- [JSON Viewer](https://wingo-ui.com/components/json-viewer) ([Markdown](https://wingo-ui.com/components/json-viewer.md))
- [Audit Log](https://wingo-ui.com/components/audit-log) ([Markdown](https://wingo-ui.com/components/audit-log.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))

---

Canonical: https://wingo-ui.com/components/diff-viewer
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
