Skip to content

DiffViewer

<DiffViewer> shows what changed between two versions. One primitive, two render paths via kind="text" | "json". Used for audit trails, version compare, workflow inbox, configuration review.

import from "@na/ui/components/DiffViewer" ▶ Open in Storybook packages/ui/src/components/DiffViewer.tsx

When to use

  • Audit log entries that need to surface “what changed” inline.
  • Version-history compare (workflow YAML, prompt edits, agent config).
  • Approval flows where the reviewer needs to see the delta from baseline.

Don’t use DiffViewer for: image / PDF diff (out of scope), real-time collaborative editing (use a CRDT-aware editor), or showing absolute values when the user doesn’t care about the change (just render the after-version).

Text mode

Line-level diff. Default mode. Whitespace and indentation are preserved.

Before
After
name: marketing-bot
prompt: |
You are a friendly outbound campaign assistant.
Removed: Aim for 3 turns max.
Added: Aim for 5 turns and follow up with an email.
tools:
- search_kb
Removed: - book_meeting
Added: - book_meeting
Added: - send_email
<DiffViewer kind="text" before={oldYaml} after={newYaml} />

Hide unchanged sections

For long files where most content is unchanged, set hideUnchanged and tune contextLines (default 3) for how much surrounding context to keep around each hunk.

Before
After
prompt: |
You are a friendly outbound campaign assistant.
Removed: Aim for 3 turns max.
Added: Aim for 5 turns and follow up with an email.
tools:
- search_kb
Removed: - book_meeting
Added: - book_meeting
Added: - send_email
<DiffViewer
kind="text"
before={oldYaml}
after={newYaml}
hideUnchanged
contextLines={2}
/>

JSON mode

Pass JS values directly — DiffViewer pretty-prints both sides and renders a key-aware structural diff.

Before
After
{
"name": "Acme Corp",
Removed: "stage": "Discovery",
Removed: "amount": 24000,
Added: "stage": "Negotiation",
Added: "amount": 32000,
"tags": [
"priority",
"enterprise",
Added: "flagged"
],
Removed: "owner": "alice"
Added: "owner": "bob"
}
<DiffViewer
kind="json"
before={{ stage: 'Discovery', amount: 24000 }}
after={{ stage: 'Negotiation', amount: 32000 }}
/>

API

Prop Type Default Description
kind "text" | "json" "text" Render mode. JSON pretty-prints both sides and runs a structural diff.
before * string | unknown Previous version. String for kind="text"; any value for kind="json".
after * string | unknown New version.
hideUnchanged boolean false Collapse long context regions between changed hunks.
contextLines number 3 Lines kept around each hunk when hideUnchanged is true.
beforeLabel ReactNode "Before" Header label for the before column.
afterLabel ReactNode "After" Header label for the after column.
className string Forwarded to the wrapper.

Design guidelines

✓ Do

  • Use kind="json" for any structured value (objects, arrays, configurations). Stringifying first then text-diffing produces noisier output.
  • Set hideUnchanged when the diff is small relative to total content. The reviewer needs to find the delta, not scroll past unchanged lines.
  • Keep beforeLabel / afterLabel concise and informative — "v3" / "v4", "Baseline" / "Proposed", not file hashes.

✗ Don't

  • Use DiffViewer for showing the after-version when nothing of importance changed. Just render the after-version.
  • Wrap DiffViewer in a tooltip or popover. The diff is information-dense; it needs space.
  • Collapse with contextLines=0. Diffs without any context are hostile — the reader can't tell what changed.

Accessibility

  • Added rows render with a visually-hidden "Added: " prefix; removed rows with "Removed: ". Color is never the only signal.
  • Line numbers and the +/ sign are aria-hidden because the prefix conveys the same meaning.
  • The collapsed-section marker () is aria-hidden — surrounding kept rows carry the structural meaning.
  • Timeline — Audit log surface where the entries are usually short; expand to a DiffViewer for “what changed”.
  • Card — Wrap DiffViewer in a Card when on a detail page.

▶ Open DiffViewer stories in Storybook