Getting Started

Overview

HTML Diff is a simple way to generate and display diffs for any app UI.

Assume you want to diff a table that's displayed to users. You render the before and after HTML, then pass both into the renderHtmlDiff function to get the diff:

import { renderHtmlDiff } from "@lix-js/html-diff";

const tableBefore = renderTable(beforeData);
const tableAfter = renderTable(afterData);

const diff = renderHtmlDiff({
  beforeHtml: tableBefore,
  afterHtml: tableAfter,
  // Optional: configure which attribute is used to match elements
  // diffAttribute: 'data-id',
});

render(diff, document.getElementById("diff-container"));

Before

|

After

|

Diff Result

|

Notice how the rendered diff automatically highlights changed cells while preserving the table structure. This works without any custom diff logic - just your existing HTML with data-diff-key attributes.

  • 🌐 Universal: Works for any app that renders to HTML (which is most apps!)
  • ⚡ Simple: No need for renderer-specific diff logic.
  • 🎨 Uses your styles: Uses your existing CSS.
  • 🔧 Framework Agnostic: Works with React, Vue, Svelte, Angular, and more.

Installation

npm install @lix-js/html-diff

Usage

Quickstart

import { renderHtmlDiff } from "@lix-js/html-diff";
import "@lix-js/html-diff/default.css";

const beforeHtml = `<p data-diff-key="x9n4" class="my-text">The quick brown fox</p>`;
const afterHtml = `<p data-diff-key="x9n4" class="my-text">The fast red fox</p>`;

const diff = renderHtmlDiff({ beforeHtml, afterHtml });

// Vanilla
document.getElementById("diff-container")!.innerHTML = diff;

// React
function Diff({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Include the default styling if you want ready-made highlights:

import "@lix-js/html-diff/default.css";

Minimal useful HTML for prose diffs:

<!-- Before -->
<article>
  <p
    data-diff-key="intro-paragraph"
    data-diff-mode="words"
    data-diff-show-when-removed
  >
    The quick brown fox
  </p>
</article>

<!-- After -->
<article>
  <p
    data-diff-key="intro-paragraph"
    data-diff-mode="words"
    data-diff-show-when-removed
  >
    The fast red fox
  </p>
</article>

Use data-diff-key to match the same element across renders, data-diff-mode="words" for inline word highlights, and data-diff-show-when-removed on elements that should stay visible when deleted.

Step 1: Adddata-diff-key attributes

Add unique data-diff-key attributes to elements you want to track for changes. Use stable semantic keys derived from durable app or content identity:

<!-- Good: keys derived from durable app/content identity -->
<div data-diff-key="user-42-profile">User content</div>
<p data-diff-key="block-introduction">Some text</p>

<!-- Avoid: keys derived only from render position -->
<p data-diff-key="paragraph-1">First paragraph</p>
<p data-diff-key="paragraph-2">Second paragraph</p>

Key guidelines:

  • Use the same key for the same element in before/after HTML
  • Only add keys to elements you want to diff (not every element)
  • Elements without keys remain unchanged in the diff
  • Avoid position-only fallback keys for ordered prose or markdown; when a paragraph is deleted, a position key can pair the deleted paragraph with the next paragraph

Programmatic key generation

In most apps you will programmatically generate the keys based on your data:

function UserCard({ user }) {
  return (
    <div>
      <h3 data-diff-key={`user-${user.id}-name`}>{user.name}</h3>
      <p data-diff-key={`user-${user.id}-email`}>{user.email}</p>
    </div>
  );
}

// Same component with different data = matching keys for diffing
const beforeHtml = renderToString(<UserCard user={oldUser} />);
const afterHtml = renderToString(<UserCard user={newUser} />);

If your source content does not already have IDs, prefer this fallback order when synthesizing keys:

  1. Existing persisted IDs from your app or content model
  2. Stable content or block IDs generated when the content is created
  3. Matching unchanged siblings by text or signature
  4. Render position only as a last resort

Step 2: Generate the diff

Use renderHtmlDiff to compare your before and after HTML:

import { renderHtmlDiff } from "@lix-js/html-diff";

const beforeHtml = `<p data-diff-key="x9n4">The quick brown fox</p>`;
const afterHtml = `
  <p data-diff-key="x9n4">The fast red fox</p>
  <p data-diff-key="k7m2">The lazy dog sleeps</p>
`;

const diff = renderHtmlDiff({ beforeHtml, afterHtml });

// The diff now contains:
// - "The fast red fox" with data-diff-status="modified"
// - "The lazy dog sleeps" with data-diff-status="added"

Step 3: Render the diff

The diff HTML preserves your original elements and styling—only data-diff-status attributes are added. Your app's existing CSS continues to work:

// Vanilla JS
document.getElementById("diff-container")!.innerHTML = diff;

// React
function DiffViewer({ diff }: { diff: string }) {
  return <div dangerouslySetInnerHTML={{ __html: diff }} />;
}
/* Your existing app styles */
.my-text {
  font-size: 24px;
  font-weight: bold;
  color: #2d3748;
  font-family: Georgia, serif;
  line-height: 1.4;
}

/* Only these selectors needed for diff highlighting */
[data-diff-status="added"] {
  color: #22c55e;
}
[data-diff-status="modified"] {
  color: #f59e0b;
}
[data-diff-status="removed"] {
  color: #ef4444;
}

Before

|

After

|

Diff Result

|

Notice how your existing .my-text styles (large, bold typography) are preserved. Only the diff highlighting is added on top based on your CSS targeting data-diff-status.

Step 4: Make diffs more granular (optional)

For word-level highlighting within text elements, add the data-diff-mode="words" attribute:

Before

|

After

|

Diff Result

|

Notice how data-diff-mode="words" enables word-level highlighting instead of highlighting the entire element. Learn more in the attributes guide.