# Diff Viewer

A line-based code comparison with split and unified views.

Page: https://sui.draco.dev/docs/components/diff-viewer

### Example: diff-viewer-demo

```tsx
"use client";
import { Button } from "@workspace/ui/components/button";
import { DiffViewer } from "@workspace/ui/components/diff-viewer";
import { useState } from "react";
import type { ExampleProps } from "../types";

const oldCode =
  'export const theme = {\n  name: "default",\n  contrast: 4.5,\n};';
const revisedCode =
  'export const theme = {\n  name: "bamboo",\n  contrast: 4.5,\n  focusContrast: 3,\n};';
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const stateLabels = chinese
    ? {
        identical: "查看相同内容",
        changes: "查看修改",
      }
    : {
        identical: "Show identical content",
        changes: "Show changes",
      };
  const [changed, setChanged] = useState(true);
  return (
    <div className="grid w-full gap-3">
      <Button
        type="button"
        variant="outline"
        className="justify-self-start"
        onClick={() => setChanged((current) => !current)}
      >
        {changed ? stateLabels.identical : stateLabels.changes}
      </Button>
      <DiffViewer
        oldCode={oldCode}
        newCode={changed ? revisedCode : oldCode}
        filename="theme.ts"
        oldTitle={
          chinese
            ? "原版本：设计令牌配置与完整的历史配色说明"
            : "Original: complete design token configuration and historical palette notes"
        }
        newTitle={chinese ? "新版本" : "Updated"}
        lang="typescript"
        labels={
          chinese
            ? {
                before: "修改前",
                after: "修改后",
                viewMode: "差异视图",
                split: "并排",
                unified: "合并",
                writing: "正在编写",
                loading: "正在高亮",
                ready: "就绪",
                error: "无法高亮差异",
                copy: "复制修改后的代码",
                copied: "已复制",
                copyFailed: "复制失败",
              }
            : undefined
        }
      />
    </div>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/diff-viewer
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

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

<DiffViewer
  oldCode={'const enabled = false;'}
  newCode={'const enabled = true;'}
  filename="settings.ts"
  lang="typescript"
/>;
```

## Split and unified views

The viewer starts with old and new code in side-by-side panes. Its view controls switch to a unified list. Added and removed lines retain distinct colors and line numbers. `oldTitle`, `newTitle`, and `filename` customize the headings. The comparison operates on lines rather than individual character changes.

Old and new sources are highlighted independently so multiline strings and comments keep each version's own syntax context. Unified unchanged lines use the new source context; split columns retain their respective source context.

Split headings stay on one line and truncate long titles, with the full text available on hover, so unequal headings cannot shift code rows. The first example pairs a long and short heading for narrow-screen comparison.

Use `maxHeight` to limit the scrolling viewport. The viewport is reachable with Tab and supports keyboard scrolling with a visible theme-colored focus ring.

## Streaming revisions

Use `status="streaming"` while `newCode` is being assembled and `"complete"` when it finishes. It follows content near the bottom, pauses after you scroll away, and resumes when you return. The example appends a local JSON revision. Copying is disabled in this example with `copyable={false}`.

### Example: diff-viewer-streaming

```tsx
"use client";
import { Button } from "@workspace/ui/components/button";
import { DiffViewer } from "@workspace/ui/components/diff-viewer";
import { useEffect, useState } from "react";
import type { ExampleProps } from "../types";

const oldCode = '{\n  "name": "default",\n  "enabled": false\n}';
const newLines = [
  "{",
  '  "name": "rose",',
  '  "enabled": true,',
  ...Array.from(
    { length: 12 },
    (_, index) => `  "item${index + 1}": ${index + 1},`,
  ),
  '  "version": 2',
  "}",
];
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const [count, setCount] = useState(newLines.length);
  const streaming = count < newLines.length;
  useEffect(() => {
    if (!streaming) return;
    const timer = setTimeout(() => setCount(count + 1), 350);
    return () => clearTimeout(timer);
  }, [streaming, count]);
  return (
    <div className="grid w-full gap-3">
      <Button
        type="button"
        variant="outline"
        className="justify-self-start"
        disabled={streaming}
        onClick={() => setCount(1)}
      >
        {chinese ? "重新播放修改" : "Replay changes"}
      </Button>
      <DiffViewer
        oldCode={oldCode}
        newCode={newLines.slice(0, count).join("\n")}
        lang="json"
        status={streaming ? "streaming" : "complete"}
        filename="settings.json"
        maxHeight={180}
        copyable={false}
        labels={
          chinese
            ? {
                before: "修改前",
                after: "修改后",
                viewMode: "差异视图",
                split: "并排",
                unified: "合并",
                writing: "正在编写",
                loading: "正在高亮",
                ready: "就绪",
                error: "无法高亮差异",
                copy: "复制修改后的代码",
                copied: "已复制",
                copyFailed: "复制失败",
              }
            : undefined
        }
      />
    </div>
  );
}
```

## Copying and localization

The copy action copies `newCode`, not a patch or the old version. Highlighting is asynchronous and falls back to plain readable lines when it fails. The surrounding theme is used unless `theme` is provided. Translate `labels` for the two versions, view controls, status, and copy feedback.

## API reference

| Prop | Type | Default / behavior |
| --- | --- | --- |
| `oldCode`, `newCode` | `string` | Required source versions. |
| `oldTitle`, `newTitle` | `string` | Optional version headings. |
| `filename` | `ReactNode` | Optional file heading. |
| `lang` | `string` | `"typescript"`. |
| `theme` | `"light" \| "dark"` | Follows surrounding theme. |
| `status` | `"streaming" \| "complete"` | `"complete"`. |
| `maxHeight` | `number` | `280` pixels. |
| `copyable` | `boolean` | `true`; copies new code. |
| `labels` | `Partial<DiffViewerLabels>` | English labels. |
| `glass` | `boolean` | `false`. |

The module exports `DiffViewerProps` and `DiffViewerLabels`. The line comparison uses [jsdiff](https://github.com/kpdecker/jsdiff).
