open source · MIT · react 18 & 19 · v0.1.0

Review diffs like an IDE, inside your React app.

a code and data diff viewer for React. side-by-side, inline and unified views, word-level highlighting, change connectors, a command palette, and JSON & CSV structural diffs, on an engine with no React or DOM dependency.

try it live ↓
>_ fig. 0 — anatomy of a review
<DiffViewer />
>_ fig. 1 — the real components, running live. edit the inputs, press ⌘K, click » or «.
views
3 split · inline · unified, + json & csv
engine only
5.06 kB gzip
complete viewer
26.54 kB gzip
largest demo file
100,000 lines

built for

  • React 18
  • React 19
  • Next.js App Router
  • Vite
  • Node 18+
  • Web Workers
  • ESM + CJS
  • TypeScript

[F] features

Everything a review needs. Nothing it doesn't.

The tools you expect from an IDE diff, as one React component: views, highlighting, connectors, a command palette, search, folding and exports.

  1. 01

    three views

    Side-by-side, inline and unified patch. Synchronized scrolling, resizable panes, and automatic inline layout on narrow screens.

    split inline unified
    >_ fig. 2.1 — views
  2. 02

    edit-aware highlighting

    Edited lines are paired by similarity, then highlighted at word or character level. Unequal line counts never break alignment.

    − return sum;
    + return Math.round(net * 100) / 100;
    >_ fig. 2.2 — word diff
  3. 03

    connectors & merge arrows

    IDE-style bands link every change to its counterpart. Hover to light up both sides; click » or « to copy a change across.

    >_ fig. 2.3 — connectors
  4. 04

    command palette

    Every action in one fuzzy list. Type : to go to a line, # to jump to a difference. Click line numbers for #R10-R20 links.

    • Go: Next differenceF7
    • Go to: Modified line 42:R42
    • Whitespace: Ignore all✓
    >_ fig. 2.4 — ⌘K
  5. 05

    JSON & CSV structural diffs

    JSON is compared as a tree of paths, with ignorable paths and keyed arrays. CSV is compared by rows and cells, matched by key columns.

    • ~ items[sku=A1].qty 2 → 3
    • + metadata.tags[2] "gift"
    • − request.legacyId
    • ≈ meta.timestamp (ignored)
    >_ fig. 2.5 — paths
  6. 06

    built for large inputs

    Virtualized rendering, highlighting of visible lines only, time and size limits, and Web Worker comparison with real cancellation.

    rendered~60 rows
    highlightedvisible only
    comparedin a worker
    >_ fig. 2.6 — 100,000 lines

[C] quick start

One import. One stylesheet.

Install the package, import the component and m2diff-viewer/styles.css. Every class is prefixed with .m2d-, so nothing leaks into the rest of your page.

>_ npm install m2diff-viewer

▾ your-app

src/App.tsx
import { DiffViewer } from 'm2diff-viewer';
import 'm2diff-viewer/styles.css';

export default function Review({ oldCode, newCode }: Props) {
  return (
    <DiffViewer
      original={oldCode}
      modified={newCode}
      originalName="src/cart.ts"
      modifiedName="src/cart.ts"
      defaultView="split"
      collapseUnchanged
      contextLines={3}
    />
  );
}

[E] engine

One engine. No UI required.

m2diff-viewer/core has no React or DOM dependency. Use it in Node, CLIs, servers, workers and tests, or build your own view on it. Server rendering produces complete markup.

normalizewhitespace · case · line endings → keystext untouched
diffMyers on interned lines, prefix/suffix trimmeddeterministic
alignpair edited lines by bigram similarityrows
intralineword or character ranges inside editsranges
outputrows · changes · stats · patch · reportDiffResult
limitsmaxLines · maxChars · timeoutMswarns, never hangs
>_ fig. 3 — what computeDiff does

[L] architecture

Open at every layer.

Use the complete viewer, compose the headless useDiff hook with your own UI, or call the engine directly. Sizes are minified and gzipped with React external.

  1. 01

    engine

    import … from 'm2diff-viewer/core'

    computeDiff, createUnifiedPatch, searchDiff, folding and reports. No React or DOM dependency.

    5.06 kB gzip
  2. 02

    structured data

    diffJson() diffCsv()

    Path-level JSON, JSONC and .env diffs, and key-matched CSV row and cell diffs, in the same engine.

    5.33 kB / 3.44 kB gzip
  3. 03

    react components

    <DiffViewer /> <JsonDiffViewer /> <CsvDiffViewer />

    The full review UI: toolbar, search, folding, connectors, command palette, status bar. Plus the headless useDiff hook.

    26.54 kB gzip
  4. 04

    highlighting adapters

    'm2diff-viewer/highlight-js' '/shiki'

    Pluggable syntax highlighting. highlight.js grammars load on demand; Shiki brings VS Code themes.

    10.58 kB / 1.56 kB gzip

[A] api reference

Everything you need before you install.

Requirements, entry points, the main props and the imperative handle. The full reference with every option is in the README.

requirements

react, react-dom^18.0.0 || ^19.0.0Optional peer. Engine-only users skip it.
highlight.js^11.0.0Optional peer, for m2diff-viewer/highlight-js.
shikiany recentOptional, for m2diff-viewer/shiki (bring your own).
diff^9.0.0The only runtime dependency (Myers line diff).
node>=18For the engine on servers, CLIs and tests.
browsersChrome/Edge 111+, Safari 16.4+, Firefox 113+Container queries and :focus-visible.
typescripttypes includedESM + CJS builds with .d.ts and .d.cts.
licenseMITFree for commercial use.

entry points

m2diff-viewerReact components, hooks, and every core export
m2diff-viewer/coreThe framework-free engine: computeDiff, diffJson, diffCsv, patches, search
m2diff-viewer/highlight-jscreateHighlightJsAdapter(): lazy grammars, one chunk per language
m2diff-viewer/shikicreateShikiAdapter(): VS Code themes and grammars via Shiki
m2diff-viewer/styles.cssThe stylesheet (6.15 kB gzip), every class scoped under .m2d-

ref handle

  • nextChange()
  • previousChange()
  • goToChange(i)
  • expandAll()
  • collapseAll()
  • openSearch(query?)
  • openCommandPalette(query?)
  • getUnifiedPatch(options?)
  • getReport()

<DiffViewer /> props

proptypedefaultdescription
original, modifiedstring | null—The texts. null means the file does not exist (added / deleted).
originalName, modifiedNamestring—Shown in headers and patches; also infer the language.
languagestringinferredLanguage id or alias ('ts', 'py'), or 'auto' to detect.
highlighterSyntaxHighlighter—From createHighlightJsAdapter() or createShikiAdapter().
view, defaultView'split' | 'inline' | 'unified' | 'auto''auto'Side-by-side from 720px, inline below.
optionsDiffOptions{}ignoreWhitespace, ignoreCase, lineEndings, intraline, limits.
collapseUnchangedbooleantrueFold unchanged regions, keeping contextLines around changes.
connectorsbooleantrueBands and » « arrows between the panes.
onApplyChange(e) => void—Makes the arrows copy a change; e.target and e.text say what to update.
selection, onSelectionChangeLineSelection | null—Lines selected from the line numbers.
syncHashboolean | stringfalseMirror the selection in the URL: #L12, #R10-R20.
theme'light' | 'dark' | 'auto''auto'auto follows prefers-color-scheme.
heightnumber | string560px'auto' grows with content.
virtualizeboolean | 'auto''auto'Render only visible rows above 400 rows.
toolbar, statusBarboolean | ToolbarOptionstrueShow, hide or pick individual controls.
keyBindingsPartial<KeyBindings> | falsesee [K]Override or disable shortcuts.
differDiffer—Compare off the main thread, e.g. createWorkerDiffer().
onDiff, onNavigate, onExportcallbacks—Result, navigation and copy/download events.

theming

Colors, fonts and sizes are CSS custom properties on .m2d-root, with light, dark and auto themes and a forced-colors mode. Override them on any wrapper.

.my-review .m2d-root {
  --m2d-font-mono: 'JetBrains Mono', monospace;
  --m2d-code-size: 12.5px;
  --m2d-row-h: 19px;          /* keep rowHeight={19} in sync */
  --m2d-accent: #0b8f6a;
  --m2d-add-bg: #e6f6ee;
  --m2d-del-bg: #fdecec;
}

[K] accessibility

Built for the keyboard.

Every control is a native button, select or input with a visible focus ring. Shortcuts are active while focus is inside the viewer, and you can override them with keyBindings.

  • ⌘/CtrlKCommand palette (also ⌘/Ctrl + Shift + P)
  • F7Shift + F7Next / previous difference (also Alt + ↓ / ↑)
  • ⌘/CtrlFSearch either or both sides
  • F3Shift + F3Next / previous match
  • :#In the palette: go to a line, jump to a difference
  • clickshift + clickSelect and extend lines from the line numbers
  • EscClear the line selection, close search or the palette
  • ←→EnterResize the panes from the divider, reset with Enter

[N] m2toolkit

More open-source packages from M2Toolkit.

[Q] faq

Questions, answered.

Is my code uploaded anywhere?

No. Everything runs in the browser (or Node). Nothing is uploaded, and no grammar or code is fetched from a CDN: highlight.js grammars are lazy chunks of your own bundle.

Can I use it with Next.js?

Yes. Components carry 'use client', so you can render them from a Server Component page with plain string props. Import m2diff-viewer/styles.css once in app/layout.tsx. Server rendering produces complete markup.

Can users merge changes, like in an IDE?

Yes. Pass onApplyChange and the » « arrows become buttons: the event carries the side to update and its new text. The viewer stays controlled, so you decide what happens next (save, undo, review).

How big a file can it handle?

The demo above includes 20,000 and 100,000 line samples. Rows are virtualized, only visible lines are highlighted, and with createWorkerDiffer() the comparison runs in a Web Worker with real cancellation. Size and time limits fall back to a simplified diff with a warning instead of freezing the page.

Can I use the engine without React?

Yes. m2diff-viewer/core has no React or DOM dependency, so it runs in Node 18+, workers, CLIs and tests. Vue or Svelte views can be built on it and share styles.css.

Is it accessible?

Every control is a native button, select or input with a visible focus ring. Changes never rely on color alone (signs and screen-reader prefixes), navigation is announced, colors meet WCAG AA in both themes, and the test suite runs axe-core on every view.

Ship a review experience your developers will love.

>_ npm install m2diff-viewer