bigbullui logobigbullui
Componenteditors

Diff Viewer

Unified diff viewer showing added/removed/context lines

Preview

Live Preview
Line 1\nLine 2\nLine 3\nLine 4\nLine 5
Line 1\nLine 2 modified\nLine 3 removed\nLine 4 added\nLine 5

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add diff-viewer

Or install the full npm package:

npm install bigbullui

Usage

import { DiffViewer } from "@/components/ui/diff-viewer";

<DiffViewer before={before} after={after} />

Source code

diff-viewer.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

interface DiffLine {
  text: string;
  type: "added" | "removed" | "context";
}

export interface DiffViewerProps extends React.HTMLAttributes<HTMLDivElement> {
  before: string;
  after: string;
  className?: string;
}

function computeDiff(
  before: string,
  after: string,
): DiffLine[] {
  // Simple diff: split by lines and find added/removed/context
  const beforeLines = before.split("\n");
  const afterLines = after.split("\n");

  const lines: DiffLine[] = [];
  let i = 0,
    j = 0;

  while (i < beforeLines.length || j < afterLines.length) {
    if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) {
      lines.push({ text: beforeLines[i], type: "context" });
      i++;
      j++;
    } else if (i < beforeLines.length) {
      lines.push({ text: beforeLines[i], type: "removed" });
      i++;
    } else {
      lines.push({ text: afterLines[j], type: "added" });
      j++;
    }
  }

  return lines;
}

export function DiffViewer({
  before,
  after,
  className,
  ...props
}: DiffViewerProps) {
  const diffLines = computeDiff(before, after);

  return (
    <div
      className={cn(
        "w-full rounded-md border border-border/60 bg-card p-4",
        "motion-reduce:transition-none",
        className,
      )}
      {...props}
    >
      <div className="space-y-2 text-[10px]">
        {diffLines.map((line, idx) => {
          const bgClass =
            line.type === "added"
              ? "bg-emerald-100 text-emerald-800"
              : line.type === "removed"
                ? "bg-destructive/10 text-destructive"
                : "bg-muted/50 text-muted-foreground";

          const transformClass =
            line.type === "added"
              ? "transform translate-x-0 transition-all duration-300"
              : line.type === "removed"
                ? "transform translate-x-[-10px] transition-all duration-300"
                : "";

          return (
            <div
              key={idx}
              className={cn(
                "p-2 rounded",
                bgClass,
                transformClass,
                "motion-reduce:transition-none",
                "animate-[slide-in_0.3s_ease-out_both]",
              )}
            >
              <span className="font-mono break-all">{line.text}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Props

PropTypeDescription
beforestringOriginal code/text.
afterstringModified code/text.