Text Utilities

Text Diff

Compare two texts and highlight added, removed and unchanged parts.

Text 11 lines
Text 21 lines

Enter text on both sides to compare differences

What is Text Diff?

Text diff compares two texts and highlights the differences between them, showing additions, deletions, and modifications.

Features

  • Line-by-line: Compare each line
  • Word-by-word: Detailed word comparison
  • Unified/Split View: Different display modes

Use Cases

  • Code review
  • Document versioning
  • Content comparison

Text Diff splits two pasted blocks on line breaks and compares them position by position, marking every row as unchanged, deleted, or added. Because line N is matched against line N, it is the right tool for confirming one known edit in a config file, a log excerpt, or a short snippet.

Comparison is by line index, not by LCS

The mainstream diff (git diff, GNU diff) first computes a longest-common-subsequence alignment and then prints a minimal edit script, so one inserted line costs exactly one + row. This tool skips alignment: it splits both blocks on the newline character, walks index 0 through max(lines) - 1, and compares the two lines sitting at the same index. Insert one line near the top and every following line is reported as deleted from text 1 and inserted from text 2, even when the two lines are byte-identical. Row volume therefore follows the offset, not the size of the real edit, and a block moved to another position is never recognised as a move.

What the two checkboxes really compare

Both toggles change the string used for the comparison, never the string that gets displayed. Ignore case lowercases each side with toLowerCase(); ignore whitespace calls only trim(), which removes leading and trailing whitespace but leaves internal runs untouched, so two consecutive spaces in the middle of a line still count as a difference, and a full-width space U+3000 is not stripped at all. Windows line endings survive the split intact as a trailing U+000D, so a CRLF block against an LF block reads as wholly rewritten until ignore-whitespace is enabled; trim() then discards that carriage return and the rows turn equal. A row that only matches after normalisation still prints the raw text 1 line, so the ignored capitals and trailing spaces stay visible.

Blank lines and the three counters

The renderer pushes a row only for non-empty content, because an empty string is falsy: a blank line can never appear as a deletion or as an addition. Delete one blank line and the index pairing shifts, so the following line is shown as inserted and then as deleted, which reads like a phantom edit rather than the blank line that actually went away. The counters are rendered-row counts, not diff statistics: one modified line always costs one delete row plus one insert row, so "deleted" includes rewritten lines and never means "removed only". Within one index the delete row is emitted before the insert row, and an unchanged row shows the text 1 copy of the line.

Worked example

One inserted line re-flags the whole tail

Input
Text 1: alpha
beta
gamma
Text 2: alpha
INSERTED
beta
gamma
Output
  alpha
- beta
+ INSERTED
- gamma
+ beta
+ gamma

Same 1 / Deleted 2 / Added 3

Only index 0 matches, because both sides hold alpha there. Index 1 pairs beta with INSERTED, index 2 pairs gamma with beta, and index 3 exists only on the right, so a single insertion is reported as 2 deletions plus 3 additions. An LCS-based diff would print one + INSERTED here instead.

Limits and pitfalls

  • No realignment: inserting or deleting one early line re-flags every line below it, so row counts grow with the offset rather than with the edit.
  • Ignore whitespace is trim() only — internal double spaces, tabs inside a line, and full-width U+3000 spaces still register as differences.
  • Blank lines are filtered out before rendering, so a change that only adds or removes an empty line is either invisible or shown as a shifted pair of rows.
  • Granularity is the whole line: a one-character edit inside a long line marks that entire line as deleted and re-added.
  • There is no patch export and no persistence — text must be pasted into the two textareas, the result is display-only, and a reload clears both panels.

Related tools

When it goes wrong

Both boxes are empty but the panel header still says 1 lines

CauseThe label is text1.split('\n').length. Splitting an empty string on the newline still yields one element, so it reports 1; a trailing newline adds another element, which is why two visible lines typed with a final Enter read as 3 lines.

FixRead it as "newline count + 1", never as visible lines. To tell whether there is any input, watch the results area — it stays on the placeholder until at least one side holds text.

The same-lines counter is one higher than the lines I can count

CauseWhen both blocks end in a newline, each split produces an empty trailing element. Both sides are '' at that index, '' === '' holds, so the row is pushed as a match; the template then renders it through item.text || ' ' as a blank row.

FixStrip the final newline from both sides before comparing, or subtract that one blank row when you quote the number.

With ignore whitespace on, a blank row present on one side only is counted as same

CauseThe missing side is read as '' and the whitespace-only line trim()s to '', so the normalised pair is equal; the display then falls back to line1 || line2 and prints the other side's raw spaces. Equal here means "equal after trim", not "present on both sides".

FixDelete the empty lines from both blocks first, then use the toggle purely to neutralise indentation differences.

The same file pasted on both sides comes out red and green top to bottom

CauseThe split only recognises U+000A, so a CRLF line keeps its trailing U+000D and index N never equals index N of the LF copy. Everything except the last row differs, and internal double spaces survive trim() too.

FixTick ignore whitespace — trim() drops that carriage return and the rows turn equal. Any pair still marked is a real in-line difference, usually a doubled or trailing space.

A 3000-line log makes the counters explode and the result pane only scrolls

CauseThe textareas carry no maxlength and the diff has no cap either, while every misaligned index emits up to two rows, so the rendered total can reach twice the line count of the longer side. The result pane is fixed at 500px with its own scrollbar, so only part of that ever reaches the screen.

FixPaste the window you actually care about — 20 lines either side of the change is enough — or run diff -u and paste just the hunk you want verified.

Ignore case is on and the same word is still marked as changed

CauseOnly toLowerCase() runs before the comparison, and it maps case without any Unicode normalisation: A (U+FF21) becomes full-width a (U+FF41) and still differs from ASCII a, while İ (U+0130) lowercases to i followed by U+0307 and never equals I.

FixNormalise to NFKC before pasting — 'A'.normalize('NFKC') is 'A' — or convert full-width letters to half-width first. The checkbox will not do it for you.

Where this page stops and a real diff starts

What you need to settleThis pageUse this instead
One known insertion, and you must show the rest of the file is untouchedRows are paired by index, so an early insertion drags the whole tail with it: put one line above line 3 of a 100-line config and the output becomes 199 rows — 2 same, 98 deleted, 99 added — for a single real edit.The same two files through git diff --no-index old.conf new.conf or diff -u: one insertion is one + line and the other 100 lines never appear. Both commands exit with status 1 when the files differ, so guard the call under set -e.
A few characters changed inside a long lineGranularity is the whole row, so that line leaves as one deletion plus one insertion. The counter says 1 deleted / 1 added and you still hunt for which characters moved.git diff --word-diff=plain, or jsdiff's diffWords() inside a script. In-line granularity is what tells you whether only the version string changed.
A block was moved rather than rewrittenA move cannot be expressed: the old position is a deletion, the new one an insertion, and the index offset pulls the untouched lines between them into the same red/green run.git diff --color-moved (Git 2.9 and later) gives a relocated block its own colour, which is the cheap way to separate a move from a rewrite.
The difference has to be stored, reviewed by someone else, or revertedThe rows exist only in the DOM. No export, a reload empties both textareas, and copying the pane yields plain text with - and + prefixes that no tool can apply.git diff --no-index a.txt b.txt > change.patch writes a patch that git apply can replay — the form a code review actually keeps.
The two blocks are one list that needs deduplication or sortingNo set operations: a line repeated three times on the left is reported three times, and reordering one side turns every following row into a delete/insert pair.Line Sort and Deduplicate: sort and dedupe each side first, then paste both results back here. Whatever still differs is a genuine difference rather than order and repetition noise.
The text must not reach a server or a temp fileThe comparison runs inside a Vue computed in the page and the component issues no network request, so pasted content never leaves the machine.Command-line diff needs the text written to disk first, and git diff may also write objects into the repository. When even a temp file is off-limits, this trade favours the page.

Frequently Asked Questions

Line diff or word diff — when is each better?
Line diff suits code and configuration, where changes are line-shaped; word diff suits prose and copy, pinpointing a single changed word in a sentence. This tool supports both: review configs in line mode, proofread contracts in word mode.
Why does it show differences in text that looks identical?
Invisible characters: trailing spaces, CRLF versus LF line endings, full-width spaces, zero-width Unicode (U+200B). The diff renders them faithfully — enable end-of-line markers to locate them. This is exactly how diff debugs "the config looks the same but does not work".
Is it safe to diff sensitive content?
Comparison is entirely local; neither text is uploaded. Still, build the habit of replacing secrets, tokens, and personal data with placeholders before pasting — diffs care about structure, and placeholders change nothing.

Content reviewed: