Diff Viewer React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React diff viewer for two texts: split or unified views, word highlights, collapsed context, syntax colors and jumps between changes.
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.
Diff Viewer is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add diff-viewer, together with Badge, Button, Copy Button, Dropdown Menu, Segmented Control and 6 more and the npm packages diff, prism-react-renderer, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add diff-viewerDiff Viewer is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import DiffViewer from @/components/ui/diff-viewer.
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
oldValue*default"# Services agreement no. 118/2026 The parties: Acme Software Inc., Chicago, Tax ID 40-1122334, as the Provider, and Tech Solutions Inc., Tax ID 12-3456789, as the Client. ## Art. 4. Scope of the agreement The Provider licenses, sets up and maintains the Acme CRM platform for 25 users, as listed in Appendix 1. Services are delivered remotely, on business days, between 9:00 AM and 6:00 PM. ## Art. 5. Payment Monthly fee: $4,990 plus sales tax The Client pays each invoice within 15 days of receiving it. Payment is made by bank transfer or in cash at the Provider's office. Late payment accrues a penalty of 0.1% per day of the amount due, capped at the value of the overdue invoice. ## Art. 6. Term The agreement runs for 12 months, starting on the date it is signed. The agreement renews automatically for further 12-month periods unless either party terminates it in writing. Notice of termination is given at least 30 days before expiry. ## Art. 7. Confidentiality The parties keep confidential the information received while performing the agreement. The obligation stays in force for 2 years after the agreement ends. Public information and information required by a competent authority are not confidential. ## Art. 8. Force majeure Force majeure releases the party that invokes it from liability, as provided by law. The affected party notifies the other party within 5 days of the event. ## Art. 9. Disputes DISPUTES are settled amicably or, failing that, by the courts of Cook County, Illinois. ## Art. 10. Final provisions This agreement was signed in two copies, one for each party. "stringThe 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*default"# Services agreement no. 118/2026 The parties: Acme Software Inc., Chicago, Tax ID 40-1122334, as the Provider, and Tech Solutions Inc., Tax ID 12-3456789, as the Client. ## Art. 4. Scope of the agreement The Provider licenses, sets up and maintains the Acme CRM platform for 40 users, as listed in Appendix 1. Services are delivered remotely, on business days, between 9:00 AM and 6:00 PM. ## Art. 5. Payment Monthly fee: $6,490 plus sales tax The Client pays each invoice within 30 calendar days of the issue date, by bank transfer to account 4839 2017 5 (routing 021000021) at Chase. Late payment accrues a penalty of 0.1% per day of the amount due, capped at the value of the overdue invoice. ## Art. 6. Term The agreement runs for 12 months, starting on the date it is signed. The agreement renews automatically for further 12-month periods unless either party terminates it in writing. Notice of termination is given at least 30 days before expiry. ## Art. 7. Confidentiality The parties keep confidential the information received while performing the agreement. The obligation stays in force for 2 years after the agreement ends. Public information and information required by a competent authority are not confidential. ## Art. 8. Force majeure Force majeure releases the party that invokes it from liability, as provided by law. The affected party notifies the other party within 5 days of the event. ## Art. 9. Disputes Disputes are settled amicably or, failing that, by the courts of Cook County, Illinois. ## Art. 10. Final provisions This agreement was signed in two copies, one for each party. Appendices 1 and 2 are an integral part of the agreement. "stringThe changed text.
oldTitledefault"Agreement v3 · Sep 12, 2026"ReactNodeThe original's name above its column (split) or first in the header (unified). Empty shows labels.original ("Original").
newTitledefault"Agreement v4 · Sep 24, 2026"ReactNodeThe changed version's name. Empty shows labels.modified ("Changed").
defaultViewdefault"auto""auto" | "split" | "unified"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") => voidCalled when the person switches the view in the toolbar or the ⋯ menu.
granularitydefault"word""word" | "char" | "none"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.
contextLinesdefault3numberUnchanged lines kept around each change; longer unchanged runs fold into an expander row. -1 shows every line.
lineNumbersdefaulttruebooleanThe old and new line number gutters. They stay pinned while long lines scroll sideways.
wrapdefaultfalsebooleanLong lines wrap instead of scrolling sideways. The ⋯ menu toggles it too; a new prop value resets it.
ignoreWhitespacedefaultfalsebooleanLines that differ only in spaces or tabs count as unchanged (and a missing final newline is ignored). Also in the ⋯ menu.
ignoreCasedefaultfalsebooleanLines that differ only in upper and lower case count as unchanged. Also in the ⋯ menu.
languagedefault""stringA 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.
showStatsdefaulttruebooleanThe +12 −3 badges in the toolbar (spoken as 12 additions, 3 deletions).
toolbardefaulttruebooleanThe toolbar with the stats, the view switch, previous / next change and the ⋯ menu. Alt+Up / Alt+Down keep working without it.
sizedefault"md""sm" | "md"Mono text 12px on 20px lines, or 13px on 22px lines.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner radius of the panel: 0, 12, 16 or 24px (full falls back to lg).
maxHeightdefault560numberHeight 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") => voidTurns 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") => ReactNodeAn extra cell in each gutter, after the numbers: comment counts, blame avatars, coverage marks.
setApi(api: DiffViewerApi) => voidReceives { nextChange(), previousChange(), expandAll(), collapseUnchanged() } once after mount; the object keeps its identity.
labelsPartial<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).
classNamesPartial<Record<"toolbar" | "header" | "title" | "body" | "table" | "row" | "number" | "sign" | "content" | "word" | "collapsed" | "empty", string>>Extra classes for the inner parts.
classNamestringExtra classes for the panel, merged last.
Parts
computeDiff
functionThe 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
constantEvery 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
typeOne 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.
Dependencies
Files
- components/ui/diff-viewer.tsx
FAQ
Is the React Diff Viewer component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Diff Viewer?
Run npx wingo-ui@latest add diff-viewer. It also adds the Wingo UI items it builds on (Badge, Button, Copy Button, Dropdown Menu, Segmented Control and 6 more) and the npm packages diff, prism-react-renderer, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/diff-viewer.
How does Diff Viewer behave on phones?
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.
See also
Used in
More React data display components
Articles using this component
- Audit Log UI in React: Filters, Diffs and CSV Export
Component guides10 min read
- React Diff Viewer in React 19: Split, Unified, Word Diffs
Component guides9 min read