File Diff

Rows are highlighted with highlight.js in a built-in GitHub palette. Set theme="custom" to drop those colors and load your own highlight.js stylesheet.

src/auth.ts +3 −1
export function getToken() {
return localStorage.token;
const t = cookies.get("session");
if (!t) throw new Error("no session");
return t;
}
<script lang="ts">
    import type { FileDiffLine } from '@sivir-ui/svelte/components/file-diff';
    import * as FileDiff from '@sivir-ui/svelte/components/file-diff';

    const diff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 12,
            newLineNumber: 12,
            content: 'export function getToken() {'
        },
        {
            type: 'remove',
            oldLineNumber: 13,
            content: '  return localStorage.token;'
        },
        {
            type: 'add',
            newLineNumber: 13,
            content: '  const t = cookies.get("session");'
        },
        {
            type: 'add',
            newLineNumber: 14,
            content: '  if (!t) throw new Error("no session");'
        },
        {
            type: 'add',
            newLineNumber: 15,
            content: '  return t;'
        },
        {
            type: 'context',
            oldLineNumber: 14,
            newLineNumber: 16,
            content: '}'
        }
    ];
</script>

<FileDiff.Root file="src/auth.ts" lang="ts" {diff} class="max-w-2xl" />

Installation

Copy the command below and run it in your terminal.

bunx @sivir-ui/svelte add file-diff

The component needs highlight.js. The add command offers to install it when it's missing. To install it yourself, run:

Copy the command below and run it in your terminal.

bun add highlight.js

Usage

Pass a diff array and Root renders the top bar and every row. The addition and deletion counts come from the add and remove lines unless you pass additions and deletions. lang takes a highlight.js language name or a common alias such as ts.

import * as FileDiff from '@sivir-ui/svelte/components/file-diff';

<FileDiff.Root file="src/auth.ts" lang="ts" diff={[
  { type: 'context', oldLineNumber: 12, newLineNumber: 12, content: 'export function getToken() {' },
  { type: 'remove', oldLineNumber: 13, content: '  return localStorage.token;' },
  { type: 'add', newLineNumber: 13, content: '  const t = cookies.get("session");' },
]} />

Examples

Composed parts

src/auth.ts +3 −1
export function getToken() {
return localStorage.token;
const t = cookies.get("session");
if (!t) throw new Error("no session");
return t;
}
<script lang="ts">
    import Expand from '@lucide/svelte/icons/expand';
    import { Button } from '@sivir-ui/svelte/components/button';
    import * as FileDiff from '@sivir-ui/svelte/components/file-diff';
</script>

<FileDiff.Root file="src/auth.ts" lang="ts" additions={3} deletions={1} class="max-w-2xl">
    <FileDiff.TopBar>
        <FileDiff.Filename />
        <FileDiff.PlusMinus />
        <Button variant="ghost" size="icon" class="size-7" aria-label="Expand diff">
            <Expand size={14} />
        </Button>
    </FileDiff.TopBar>
    <FileDiff.Content>
        <FileDiff.Row
            type="context"
            oldLine={12}
            newLine={12}
            code={'export function getToken() {'}
        />
        <FileDiff.Row type="remove" oldLine={13} code="  return localStorage.token;" />
        <FileDiff.Row type="add" newLine={13} code={'  const t = cookies.get("session");'} />
        <FileDiff.Row type="add" newLine={14} code={'  if (!t) throw new Error("no session");'} />
        <FileDiff.Row type="add" newLine={15} code="  return t;" />
        <FileDiff.Row type="context" oldLine={14} newLine={16} code={'}'} />
    </FileDiff.Content>
</FileDiff.Root>

Leave out diff and compose TopBar, Content, and Row yourself. Root can't count rows you compose, so pass additions and deletions. Children replace the TopBar's default filename and counts; add Filename and PlusMinus back next to your own actions.

Without line numbers

src/styles.css +1 −1
color: var(--color-foreground);
background: #f4f4f0;
background: var(--color-card);
border: 1px solid var(--color-border);
<script lang="ts">
    import type { FileDiffLine } from '@sivir-ui/svelte/components/file-diff';
    import * as FileDiff from '@sivir-ui/svelte/components/file-diff';

    const diff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 4,
            newLineNumber: 4,
            content: '  color: var(--color-foreground);'
        },
        {
            type: 'remove',
            oldLineNumber: 5,
            content: '  background: #f4f4f0;'
        },
        {
            type: 'add',
            newLineNumber: 5,
            content: '  background: var(--color-card);'
        },
        {
            type: 'context',
            oldLineNumber: 6,
            newLineNumber: 6,
            content: '  border: 1px solid var(--color-border);'
        }
    ];
</script>

<FileDiff.Root file="src/styles.css" lang="css" {diff} showLineNumbers={false} class="max-w-2xl" />

Set showLineNumbers to false to hide both gutters. The + and − column stays, so changes are readable without color.

Stacked files

src/auth.ts +3 −1
export function getToken() {
return localStorage.token;
const t = cookies.get("session");
if (!t) throw new Error("no session");
return t;
}
src/styles.css +1 −1
color: var(--color-foreground);
background: #f4f4f0;
background: var(--color-card);
border: 1px solid var(--color-border);
<script lang="ts">
    import type { FileDiffLine } from '@sivir-ui/svelte/components/file-diff';
    import * as FileDiff from '@sivir-ui/svelte/components/file-diff';

    const authDiff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 12,
            newLineNumber: 12,
            content: 'export function getToken() {'
        },
        {
            type: 'remove',
            oldLineNumber: 13,
            content: '  return localStorage.token;'
        },
        {
            type: 'add',
            newLineNumber: 13,
            content: '  const t = cookies.get("session");'
        },
        {
            type: 'add',
            newLineNumber: 14,
            content: '  if (!t) throw new Error("no session");'
        },
        {
            type: 'add',
            newLineNumber: 15,
            content: '  return t;'
        },
        {
            type: 'context',
            oldLineNumber: 14,
            newLineNumber: 16,
            content: '}'
        }
    ];

    const stylesDiff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 4,
            newLineNumber: 4,
            content: '  color: var(--color-foreground);'
        },
        {
            type: 'remove',
            oldLineNumber: 5,
            content: '  background: #f4f4f0;'
        },
        {
            type: 'add',
            newLineNumber: 5,
            content: '  background: var(--color-card);'
        },
        {
            type: 'context',
            oldLineNumber: 6,
            newLineNumber: 6,
            content: '  border: 1px solid var(--color-border);'
        }
    ];
</script>

<div class="flex w-full max-w-2xl flex-col gap-4">
    <FileDiff.Root file="src/auth.ts" lang="ts" diff={authDiff} />
    <FileDiff.Root file="src/styles.css" lang="css" diff={stylesDiff} />
</div>

Render one Root per file, as in a pull request. Each Root has its own language and counts.

Anatomy

FileDiff.Root - Holds the file, language, and counts; renders everything when given diff.

FileDiff.TopBar - Renders the file path and counts, or your own children.

FileDiff.Filename - Renders the file path.

FileDiff.PlusMinus - Renders the addition and deletion counts.

FileDiff.Content - Contains the scrollable diff rows.

FileDiff.Row - Renders one highlighted diff row.

FileDiff.LineNumber - Renders one gutter line number.