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.
- 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.
- 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 - 02
edit-aware highlighting
Edited lines are paired by similarity, then highlighted at word or character level. Unequal line counts never break alignment.
− returnsum;+ return Math.round(net * 100) / 100;>_ fig. 2.2 — word diff - 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 - 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 - 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 - 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 rowshighlightedvisible onlycomparedin 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
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.
[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.
- 01
engine
import … from 'm2diff-viewer/core'computeDiff, createUnifiedPatch, searchDiff, folding and reports. No React or DOM dependency.
5.06 kB gzip - 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 - 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 - 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.0 | Optional peer. Engine-only users skip it. |
|---|---|---|
| highlight.js | ^11.0.0 | Optional peer, for m2diff-viewer/highlight-js. |
| shiki | any recent | Optional, for m2diff-viewer/shiki (bring your own). |
| diff | ^9.0.0 | The only runtime dependency (Myers line diff). |
| node | >=18 | For the engine on servers, CLIs and tests. |
| browsers | Chrome/Edge 111+, Safari 16.4+, Firefox 113+ | Container queries and :focus-visible. |
| typescript | types included | ESM + CJS builds with .d.ts and .d.cts. |
| license | MIT | Free for commercial use. |
entry points
m2diff-viewer | React components, hooks, and every core export |
|---|---|
m2diff-viewer/core | The framework-free engine: computeDiff, diffJson, diffCsv, patches, search |
m2diff-viewer/highlight-js | createHighlightJsAdapter(): lazy grammars, one chunk per language |
m2diff-viewer/shiki | createShikiAdapter(): VS Code themes and grammars via Shiki |
m2diff-viewer/styles.css | The 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
| prop | type | default | description |
|---|---|---|---|
original, modified | string | null | — | The texts. null means the file does not exist (added / deleted). |
originalName, modifiedName | string | — | Shown in headers and patches; also infer the language. |
language | string | inferred | Language id or alias ('ts', 'py'), or 'auto' to detect. |
highlighter | SyntaxHighlighter | — | From createHighlightJsAdapter() or createShikiAdapter(). |
view, defaultView | 'split' | 'inline' | 'unified' | 'auto' | 'auto' | Side-by-side from 720px, inline below. |
options | DiffOptions | {} | ignoreWhitespace, ignoreCase, lineEndings, intraline, limits. |
collapseUnchanged | boolean | true | Fold unchanged regions, keeping contextLines around changes. |
connectors | boolean | true | Bands and » « arrows between the panes. |
onApplyChange | (e) => void | — | Makes the arrows copy a change; e.target and e.text say what to update. |
selection, onSelectionChange | LineSelection | null | — | Lines selected from the line numbers. |
syncHash | boolean | string | false | Mirror the selection in the URL: #L12, #R10-R20. |
theme | 'light' | 'dark' | 'auto' | 'auto' | auto follows prefers-color-scheme. |
height | number | string | 560px | 'auto' grows with content. |
virtualize | boolean | 'auto' | 'auto' | Render only visible rows above 400 rows. |
toolbar, statusBar | boolean | ToolbarOptions | true | Show, hide or pick individual controls. |
keyBindings | Partial<KeyBindings> | false | see [K] | Override or disable shortcuts. |
differ | Differ | — | Compare off the main thread, e.g. createWorkerDiffer(). |
onDiff, onNavigate, onExport | callbacks | — | 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