Diff Views & Change Tracking UI: Chat Prompts
Any CRUD app with version history eventually needs a way to show what changed. A doc editor needs "compare to last version." A settings page needs "what did this rule used to look like?" An audit log needs "show me the diff for this change." A config editor needs to highlight which lines a deploy will modify.
Diff UI is one of those features that looks trivial in a Figma but rewards careful implementation: line vs. word vs. character granularity, side-by-side vs. unified, structured-data diffs (JSON / arrays / nested objects) vs. plain text diffs, syntax highlighting, large-document performance, expand/collapse around unchanged regions, and accessibility (color isn't enough — patterns + symbols too). Get it right and your version history feels first-class. Get it wrong and users squint at red/green walls of text and bounce.
When You Need a Diff View
Use diff UI for:
- Document editors with revision history (compare any two versions)
- Settings / config pages where audit + revert matter
- Code review surfaces (you're not writing GitHub, but maybe a config or template review flow)
- Audit logs where the change is more important than the timestamp
- Any "your changes" preview before save (deploy, publish, apply)
Don't use diff UI for:
- A timeline of events (use an activity feed instead)
- Single-field edits — just show old → new inline
- Anything where the user only needs to see the current state
Text Diff: Plain Text / Markdown / Documents
The most common diff. Two strings; show what's added, removed, unchanged. Libraries: diff (jsdiff), diff-match-patch, react-diff-view, react-diff-viewer-continued.
I want to add a "compare versions" diff view to my [resource, e.g. "blog post draft"] editor.
Stack: Next.js App Router + TypeScript. Editor outputs plain text or Markdown.
Use the `diff` library (jsdiff). I want:
1. Two-pane side-by-side view: old version on left, new on right
2. Inline mode toggle: combined unified-diff view as alternative
3. Line-level diff (default) with word-level highlight inside changed lines
4. Color: green for additions, red for deletions, neutral for unchanged
5. Plus icons / minus icons for accessibility (color isn't enough — symbols too)
6. Expand/collapse around large unchanged regions ("3 unchanged lines [+]")
7. Sticky line numbers
8. Copy-button per side
Implement:
- A `<DiffView old={oldText} new={newText} mode="side-by-side" | "unified" />` component
- Use react-diff-viewer-continued or build with jsdiff + custom rendering
- Tailwind for styling; supports dark mode
- Accessible: ARIA labels for each change region; high-contrast mode
Bonus:
- Word-wrap toggle
- "Jump to next change" keyboard shortcut (J / N)
Output: a complete <DiffView> component with both modes, the toggle, accessibility, and wrap behavior.
Structured Data Diff: JSON / Settings / Configs
Plain text diff is wrong for JSON. {"a":1, "b":2} vs {"b":2, "a":1} are equivalent but a text diff says they're different. Use a structured diff that walks the object tree.
I have a [resource, e.g. "feature flag config"] stored as JSON. I want a diff view that:
1. Compares the structured object, not the serialized text
2. Shows changes per-key, not per-line
3. Output: tree view with each key marked added / removed / changed
4. For changed leaf values: show old → new inline
5. For nested objects: drill in to show inner changes
Tools to consider: `deep-diff`, `microdiff`, `json-diff`, or hand-roll using lodash isEqual + recursion.
Implement:
- Function `diffJson(old, new)` returning a structured diff tree:
[ { path: ['features', 'darkMode'], type: 'added', newValue: true }, { path: ['features', 'beta'], type: 'changed', oldValue: false, newValue: true }, { path: ['users', 0, 'role'], type: 'removed', oldValue: 'admin' }, ]
- A `<JsonDiff old={oldObj} new={newObj} />` component rendering the tree with visual hierarchy
- Color + icons (added: green +, removed: red -, changed: yellow ~)
- Expand/collapse for unchanged subtrees
- Show full path for each change ("features → darkMode") for context
Stack: Next.js + TypeScript + microdiff or deep-diff.
Inline "What Changed" Preview Before Save
Show users what they're about to commit before they hit Save. Reduces "wait, I didn't mean that" support tickets.
On my settings page, when the user has pending changes, show a "Review changes" panel listing exactly what they're about to save.
Pattern:
1. Track form state vs. last-saved server state in TanStack Query
2. Compute diff: which fields changed?
3. Show a sticky bottom panel: "X changes pending — Review"
4. Click "Review" opens a modal/panel listing each change: `Field name: oldValue → newValue`
5. User can confirm "Save all" or revert specific fields with a small undo button
6. Hide panel when no changes
Implement:
- Hook `usePendingChanges<T>(form, server)` returning structured diff
- The bottom panel + review modal
- Per-field revert + "revert all" actions
- Pattern works for nested objects (e.g. `notifications.email.weekly: true → false`)
Bonus: show a "summary" sentence — "Updating 3 settings, removing 1 webhook, adding 1 domain"
Stack: Next.js + react-hook-form + TanStack Query.
Code / Config Diff with Syntax Highlighting
For config files, code snippets, query templates — adding syntax highlighting on top of the diff makes the changes scannable.
I'm building a config editor for [thing, e.g. "GitHub Actions workflow YAML"]. Diff view should:
1. Show two-pane side-by-side YAML
2. Syntax highlight via Shiki / Prism / CodeMirror
3. Highlight changed lines with green/red overlay (preserving syntax color)
4. Highlight changed *tokens* within a line (word-level diff inside the changed line)
5. Show "@@ -line,count +line,count @@" hunk headers for reference
6. Collapse 5+ contiguous unchanged lines with [+] expand
Stack: Next.js + Shiki for syntax highlighting + diff-match-patch for char-level diff.
Implement:
- `<CodeDiff lang="yaml" old={...} new={...} />`
- Shiki highlights both sides; diff overlay on top
- Word-level diff inside changed lines
- Pure CSS for the green/red overlay (semi-transparent so syntax color shows through)
- Dark mode
Bonus: GitHub-style "expand 5 lines" buttons at hunk boundaries.
Side-by-Side vs. Unified View
Some users prefer side-by-side; some prefer unified (GitHub default). Let them choose.
Add a view-mode toggle to my diff component: "Side-by-side" | "Unified".
Behavior:
- Side-by-side (default for wide screens): old left, new right, line numbers per side
- Unified: single column, additions and deletions interleaved with `+` / `-` line prefix
- Auto-switch to unified on narrow screens (< 768px) regardless of preference
- Persist user preference in localStorage
Implement:
- Hook `useDiffViewMode()` reading/writing preference
- `<DiffView mode={mode} ... />` accepting both modes
- Auto-narrow detection via CSS or window resize listener
Stack: Tailwind + Next.js.
Diff for Audit Logs (Showing Past Changes)
Audit logs frequently say "User X updated field Y" without showing the content of the change. Add diff inline.
My audit log shows entries like "[user] updated [resource] at [time]". I want to expand each entry to show the actual diff between the before and after state.
Data model: each audit log entry stores `before` and `after` snapshots (JSON).
UI:
1. Audit log row collapsed: timestamp, user, action ("updated billing settings")
2. Click to expand: shows JSON diff of before/after
3. For text-heavy fields (name, description), show inline word-level diff
4. For boolean/enum fields, show old → new inline
5. For complex nested changes, show path + change
Implement:
- Server: ensure audit log captures `before` and `after` (or computed diff at write time for storage efficiency)
- Component: `<AuditEntry entry={entry} />` with expandable diff
- Reuse the JsonDiff component from earlier
- Be performance-aware: don't render diff for thousands of audit entries at once — render on expand
Stack: Next.js + my JsonDiff from above.
Three-Way Merge Conflict UI
When two users edited the same doc and conflict, show a three-way diff: base (common ancestor), theirs, yours. Let user pick.
Two users edited the same draft. Server returned a 409 conflict. I have:
- `base`: the version both users started from
- `theirs`: the version saved by the other user
- `yours`: the local version
I want a merge UI that:
1. Shows three columns: base | theirs | yours
2. Highlights regions where theirs and yours differ from base
3. For each conflict region, "Use theirs" / "Use yours" / "Keep mine merged" buttons
4. Output: a merged version the user explicitly assembled
5. Save sends the merged version + new expectedVersion
Hard parts:
- Detecting conflict regions (sections where both theirs and yours diverge from base)
- Auto-merge non-conflicting regions (theirs adds paragraph 5; yours edits paragraph 2; auto-merge to apply both)
- UI for confirming the auto-merge and reviewing remaining conflicts
Suggest: use `diff3` library or implement Myers diff with three-way merge.
Implement the UI component + the merge logic.
Stack: Next.js + diff3.
Performance: Large Documents
A 10KB document is fine. A 1MB document with character-level diff freezes the browser.
My documents can be very large (up to 200KB plain text). My current naive diff implementation freezes the browser computing the diff client-side.
Strategies:
1. Move diff computation server-side; ship pre-computed diff to client
2. Or: compute on a Web Worker so the main thread isn't blocked
3. Limit diff granularity for large docs — line-level only, no word-level inside lines past a threshold
4. Virtualize the diff render — only render visible diff regions (react-virtual / virtualization)
Implement option 2: a Web Worker for diff computation. The worker:
- Receives `{old, new, mode}` from main thread
- Computes diff using jsdiff
- Returns structured diff result
- Main thread renders the result
Show me:
- The worker file (TypeScript)
- The hook `useDiff(old, new, mode)` that posts to worker and returns result
- Loading state while computing
- Cancel previous worker when new diff requested
Stack: Next.js + jsdiff + Web Worker.
Accessibility
Color alone is not enough. Some users are colorblind; some use high-contrast modes; some use screen readers.
My diff view uses red/green for added/removed. Make it accessible:
1. Add icons / symbols: + for additions, - for deletions, ~ for changes
2. Aria labels: each change region has `aria-label="Added: ..."` or `aria-label="Removed: ..."`
3. Screen reader announcements summarize: "5 additions, 3 deletions, 2 changes"
4. Keyboard navigation: J/K or arrow keys move between changes; Tab focuses the next change region
5. Don't rely solely on color — use background color + border + icon
High-contrast mode:
- CSS @media (prefers-contrast: more) overrides
- Stronger borders; thicker icons
Implement:
- Update existing DiffView component with these accessibility additions
- Provide a "Listen" button that screen-reader narrates the diff (optional)
Stack: Tailwind + Next.js.
Common Pitfalls
Plain text diff on structured data. JSON / YAML / config that's "the same" but reordered shows as completely changed. Use a structured diff.
No collapse on long unchanged regions. Users scroll through 200 lines of unchanged code to find the 3 changed ones. Always offer collapse.
Color-only signaling. ~8% of male users are red-green colorblind. Add icons + patterns + aria labels.
Character-level diff on huge documents. A 100KB doc with character-level diff is unworkable. Drop to line-level past a threshold.
Synchronizing scroll mishandled in side-by-side. Scrolling one pane should scroll the other. Forgetting this is jarring.
No "show changes only" filter. When the doc is mostly unchanged, users want to hide the noise. Offer "show only changes."
Word-level diff that splits mid-word. Bad whitespace handling causes "userName" → "user_name" to highlight as 8 character changes instead of 1 token swap. Use a word-aware tokenizer.
No keyboard navigation. Mouse-only diff UI fails for power users. J/K or arrow keys to jump between changes.
Side-by-side on narrow screens. Two columns of diff at 320px wide is unreadable. Auto-switch to unified.
Computing diff on every render. Memoize. The diff is expensive; recompute only when inputs change.
Audit log without change detail. "User X updated billing" doesn't tell you what changed. Always store before+after snapshots and render diff.
Sensitive data in diffs. Passwords, API keys, PII shown verbatim in audit logs is a leak. Redact sensitive fields (password: "***" → "***").
Trailing whitespace / line ending differences as visual noise. A diff dominated by \r\n vs \n differences hides real changes. Normalize line endings before diffing.
Markdown / rich-text edits diffed as raw source. When the user edited a TipTap doc, diffing the raw HTML/JSON source is alienating. Render a "rendered" diff (the doc as it appears) when possible, fall back to source diff for technical users.
Forgetting "no changes" empty state. When old and new are identical, the diff view should say so explicitly, not just render blank.
Saving the diff on the server when you can compute it. Some teams persist the computed diff to save CPU. Storage cost is real; usually compute on-demand from snapshots is better.
See Also
- Activity Feed / Timeline Implementation
- Audit Logs
- Optimistic UI Updates
- Form Autosave & Draft Persistence
- Real-Time Collaboration
- Realtime Presence & Collaborative Cursors
- Rich Text Editor Implementation
- Form Validation UX
- Inline Editing Patterns
- Internal Admin Tools
- Soft Delete vs Hard Delete
- Customer Managed Encryption Keys (BYOK) — for encrypted-at-rest data needing diff