---
title: "Compare document versions"
description: "Compare snapshots of your documents to see changes made between two versions."
canonical_url: "https://tiptap.dev/docs/collaboration/documents/snapshot-compare"
---

# Compare document versions

Compare snapshots of your documents to see changes made between two versions.

Snapshot Compare lets you line-up two document versions and highlights everything that changed. Use it to track edits, review contributions, and restore earlier states.

The Snapshot Compare extension adds extra functionality to the [Snapshots](https://tiptap.dev/docs/collaboration/documents/snapshot.md) by allowing you to visually compare changes made between two versions of a document so you can track what’s been added, removed, or modified. These comparisons are called *diffs*.

- **1. Activate trial or subscribe**

  Start a [free trial](https://cloud.tiptap.dev/v2?trial=true) or [subscribe to the Team
  plan](https://cloud.tiptap.dev/v2/billing) in your account.
- **2. Start Document server**

  [Add an Environment](https://cloud.tiptap.dev/v2/configuration/document-server) in your
  dashboard and configure your [Document
  server](https://cloud.tiptap.dev/v2/configuration/document-server).
- **3. Install from private registry**

  To install this extension, authenticate to Tiptap’s private npm registry by following the [setup
  guide](https://tiptap.dev/docs/guides/pro-extensions.md).

> **Interactive demo:** [SnapshotCompare](https://embed-pro.tiptap.dev/preview/Extensions/SnapshotCompare)

## Access the private registry

The Snapshot Compare extension is published in Tiptap’s private npm registry. Integrate the extension by following the [private registry guide](https://tiptap.dev/docs/guides/pro-extensions.md). If you already authenticated your Tiptap account you can go straight to [#Install](#install).

## Install

Install the extension from our private registry:

```bash
npm install @tiptap-pro/extension-snapshot-compare
```

## Settings

You can configure the `SnapshotCompare` extension with the following options:

| Setting              | Type                   | Default    | Description                                                       |
| -------------------- | ---------------------- | ---------- | ----------------------------------------------------------------- |
| provider             | `TiptapCollabProvider` | `null`     | The Collaboration provider instance                               |
| mapDiffToDecorations | `function`             | `() => {}` | Control mapping a diff into a decoration to display their content |

Note that you need to provide the `user` identifier to the `TiptapCollabProvider`, as this information is used to optimize diffs.

```js
const provider = new TiptapCollabProvider({
  // ...
  user: 'your user identifier' // REQUIRED! Note that we use the user identifier to optimize diffs, so it's important to provide it.
})

const editor = new Editor({
  // ...
  extensions: [
    // ...
    SnapshotCompare.configure({
      provider,
    }),
  ],
})
```

#### Using `mapDiffToDecorations` for diff decorations

The extension has a default mapping (`defaultMapDiffToDecorations`) to represent diffs as ProseMirror decorations.
For more complex integrations and control, you can customize this mapping with the `mapDiffToDecorations` option.

**Example:** Applying custom predefined background colors to inline inserts

```ts
SnapshotCompare.configure({
  mapDiffToDecorations: ({ diff, tr, editor, defaultMapDiffToDecorations }) => {
    if (diff.type === 'inline-insert') {
      // return ProseMirror decoration(s) or null
      return Decoration.inline(
        diff.from,
        diff.to,
        {
          class: 'diff',
          style: {
            backgroundColor: diff.attribution.color.backgroundColor,
          },
        },
        // pass the diff as the decoration's spec, this is required for `extractAttributeChanges`
        { diff },
      )
    }

    // fallback to the default mapping
    return defaultMapDiffToDecorations({
      diff,
      tr,
      editor,
      attributes: {
        // add custom attributes to the decorations
        'data-tiptap-user-id': myUserIdMapping[diff.attribution.userId],
      },
    })
  },
})
```

## Storage

The `SnapshotCompare` storage object contains the following properties:

| Key             | Type                  | Description                                   |
| --------------- | --------------------- | --------------------------------------------- |
| isPreviewing    | `boolean`             | Indicates whether the diff view is active     |
| diffs           | `Diff[]`              | The diffs that are displayed in the diff view |
| previousContent | `JSONContent \| null` | The content before the diff view was applied  |

Use the `isPreviewing` property to check if the diff view is currently active:

```ts
if (editor.storage.snapshotCompare.isPreviewing) {
  // The diff view is currently active
}
```

Use the `diffs` property to access the diffs displayed in the diff view:

```ts
editor.storage.snapshotCompare.diffs
```

The property `previousContent` is used internally by the extension to restore the content when exiting the diff view. Typically, you do not need to interact with it directly.

## Commands

| Command           | Description                                                        |
| ----------------- | ------------------------------------------------------------------ |
| `compareVersions` | Diffs two versions and renders the diff within the editor          |
| `showDiff`        | Given a change tracking transform, show the diff within the editor |
| `hideDiff`        | Hide the diff and restore the previous content                     |

### compareVersions

Use the `compareVersions` command to compute and display the differences between two document versions.

```ts
editor.chain().compareVersions({
  fromVersion: 1,
})
```

#### Options

You can pass in additional options for more control over the diffing process:

| Key               | Description                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `fromVersion`     | The version to compare from. The order between `fromVersion` and `toVersion` is flexible |
| `toVersion`       | The version to compare to (default: latest version)                                      |
| `hydrateUserData` | Add contextual data to each user's changes                                               |
| `onCompare`       | Handle the diffing result manually                                                       |
| `enableDebugging` | Enable verbose logging for troubleshooting                                               |

#### Using `hydrateUserData` to add metadata

Each diff has an `attribution` field, which allows you to add additional metadata with the `hydrateUserData` callback function.

> **Note:**
>
> Do note that the `userId` is populated by the `TiptapCollabProvider` and should be used to
> identify the user who made the change. Without the `user` field provided by the provider, the
> `userId` will be `null`. [See more information in the TiptapCollabProvider
> documentation](https://tiptap.dev/docs/collaboration/provider/integration.md).

**Example:** Color-coding diffs based on user

```ts
const colorMapping = new Map([
  ['user-1', '#ff0000'],
  ['user-2', '#00ff00'],
  ['user-3', '#0000ff'],
])

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  hydrateUserData: ({ userId }) => {
    return {
      color: {
        backgroundColor: colorMapping.get(userId),
      },
    }
  },
})

editor.storage.snapshotCompare.diffs[0].attribution.color.backgroundColor // '#ff0000'
```

#### Using `onCompare` to customize the diffing process

If you need more control over the diffing process, use the `onCompare` option to receive the result and handle it yourself.

**Example:** Filtering diffs by user

```ts
editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  onCompare: (ctx) => {
    if (ctx.error) {
      // handle errors that occurred in the diffing process
      console.error(ctx.error)
      return
    }

    // filter the diffs to display only the changes made by a specific user
    const diffsToDisplay = ctx.diffSet.filter((diff) => diff.attribution.userId === 'user-1')

    editor.commands.showDiff(ctx.tr, { diffs: diffsToDisplay })
  },
})
```

### showDiff

Use the `showDiff` command to display the diff within the editor using a change tracking transform (`tr`). This represents all of the changes made to the document since the last snapshot. You can use this transform to show the diff in the editor.

Typically, you use this command after customizing or filtering diffs with `compareVersions`, `onCompare`.

The `showDiff` command temporarily replaces the current editor content with the diff view, showing the differences between versions. It also stashes the content currently displayed in editor so that you can restore it later.

```ts
// This will display the changes that change tracking transform recorded in the editor
editor.commands.showDiff(tr)
```

### Options

You can pass additional options to control how the diffs are displayed:

| Key   | Description                    |
| ----- | ------------------------------ |
| diffs | An array of diffs to visualize |

**Example:** Displaying specific diffs

```ts
// This will display only the diffs made by the user with the ID 'user-1'
const diffsToDisplay = tr.toDiff().filter((diff) => diff.attribution.userId === 'user-1')

editor.commands.showDiff(tr, { diffs: diffsToDisplay })
```

### hideDiff

Use the `hideDiff` command to hide the diff and restore the previous content.

```ts
// This will hide the diff view and restore the previous content
editor.commands.hideDiff()
```

## Add styles

The Snapshot Compare extension applies classes to the elements of the diff view to help you style inserted and deleted text. See a complete example of how to style the diff view in the [code demo](#page-title) at the top of this page.

### Basic diff styling

The extension applies the following attributes to diff elements:

- `data-diff-type="inline-insert"` - for inserted text
- `data-diff-type="inline-delete"` - for deleted text
- `data-diff-type="inline-update"` - for updated text
- `data-diff-type="block-insert"` - for inserted blocks
- `data-diff-type="block-delete"` - for deleted blocks

You can target the elements with these data attributes by using CSS selectors.

Here's an example of basic styling for inserted and deleted text:

```css
/* When there is no user involved, fallback to red and green colors */
/* Style inserted text */
[data-diff-type='inline-insert'],
[data-diff-type='inline-update'],
[data-diff-type='block-insert'] {
  background-color: green;
}

/* Style deleted text */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  background-color: red;
  text-decoration: line-through;
}
```

### Resetting mark styles for deleted text

When styling marks are applied (like **bold**, *italic*, or `code`), the deleted text (the text before the formatting was applied) can have that formatting applied to it. This issue occurs because the deleted text appears inside the HTML tag of the formatting. To prevent this, reset the mark styles inside the deleted content.

First, reset the outer mark styles inside the deleted content.

```scss
/* Reset strong/bold formatting for deleted content */
strong {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-weight: normal;
  }
}

/* Reset italic formatting for deleted content */
em {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-style: normal;
  }
}

/* Reset code formatting for deleted content */
code {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-family: sans-serif;
  }
}
```

Then, if there is a mark tag inside the deleted content, re-apply the mark styles to it. This will ensure that the formatting is correctly applied to the deleted content.

```scss
/* Ensure mark styles work properly inside the deleted content */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  strong {
    font-weight: bold;
  }
  em {
    font-style: italic;
  }
  code {
    font-family: monospace;
  }
}
```

### User attribution styling

When user attribution is available, you can style diffs based on the user who made the change:

```css
/* Style diffs with user attribution */
[data-diff-user-id] {
  position: relative;
}

/* Tooltip showing user name */
[data-diff-user-id]::before {
  content: attr(data-diff-user-id);
  position: absolute;
  visibility: hidden;
}

[data-diff-user-id]:hover::before {
  visibility: visible;
}
```

## Working with NodeView (advanced)

When using [custom node views](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views.md), the default diff mapping may not work as expected. You can customize the mapping and render the diffs directly within the custom node view.

Use the `extractAttributeChanges` helper to extract attribute changes in nodes. This allows you to access the previous and current attributes of a node, making it possible to highlight attribute changes within your custom node views.

> **Note:**
>
> When mapping the diffs into decorations yourself, you need to pass the `diff` as the
> decoration's `spec`. This is required for `extractAttributeChanges` to work correctly.

**Example:** Customizing a heading node view to display changes

```ts
import { extractAttributeChanges } from '@tiptap-pro/extension-snapshot-compare'

const Heading = BaseHeading.extend({
  addNodeView() {
    return ReactNodeViewRenderer(({ node, decorations }) => {
      const { before, after, isDiffing } = extractAttributeChanges(decorations)

      return (
        <NodeViewWrapper style={{ position: 'relative' }}>
          {isDiffing && before.level !== after.level && (
            <span
              style={{
                position: 'absolute',
                right: '100%',
                fontSize: '14px',
                color: '#999',
                backgroundColor: '#f0f0f070',
              }}
              // Display the difference in level attribute
            >
              #<s>{before.level}</s>
              {after.level}
            </span>
          )}
          <NodeViewContent as={`h${node.attrs.level ?? 1}`} />
        </NodeViewWrapper>
      )
    })
  },
})
```

## Technical details

### Diff

A `Diff` is an object that represents a change made to the document. It contains the following properties:

| Property              | Type                                                                                                                    | Description                                                                                           |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `type`                | `'inline-insert'` \| `'inline-delete'` \| `'block-insert'` \| `'block-delete'` \| `'inline-update'` \| `'block-update'` | The type of change made                                                                               |
| `from`                | `number`                                                                                                                | The start position of the change                                                                      |
| `to`                  | `number`                                                                                                                | The end position of the change                                                                        |
| `content`             | `Fragment`                                                                                                              | The content that was added or removed                                                                 |
| `attribution`         | `Attribution`                                                                                                           | Metadata about the change, such as the user who made the change                                       |
| `attributes?`         | `Record<string, any>`                                                                                                   | The attributes **after** the change; only available for `'inline-update'` and `'block-update'` diffs  |
| `previousAttributes?` | `Record<string, any>`                                                                                                   | The attributes **before** the change; only available for `'inline-update'` and `'block-update'` diffs |

### DiffSet

A `DiffSet` is an array of `Diff` objects, each corresponding to a specific change, like insertion, deletion, or update. You can iterate over the array to inspect individual changes or apply custom logic based on the diff types.

```ts
const diffsToDisplay = diffSet.filter((diff) => diff.attribution.userId === 'user-1')

// Show the filtered diffs in the editor
editor.commands.showDiff(tr, { diffs: diffsToDisplay })
```

### Attribution

The `Attribution` object contains metadata about a change. It includes the following properties:

| Property | Type                     | Description                                        |
| -------- | ------------------------ | -------------------------------------------------- |
| `type`   | `'added'` \| `'removed'` | Indicates the type of change made                  |
| `userId` | `string` \| `undefined`  | The ID of the user who made the change             |
| `id`     | `Y.ID` \| `undefined`    | The Y.js client ID of the user who made the change |

You can extend the `Attribution` interface to include additional properties:

```ts
declare module '@tiptap-pro/extension-snapshot-compare' {
  interface Attribution {
    userName: string
  }
}
```

### ChangeTrackingTransform

The `ChangeTrackingTransform` is a class that records changes made to the document (based on ProseMirror's `Transform`).
It represents a transform whose steps describe all of the changes made to go from one version of the document to another. It has the following properties:

| Property | Type                   | Description                                                       |
| -------- | ---------------------- | ----------------------------------------------------------------- |
| `steps`  | `ChangeTrackingStep[]` | An array of steps that represent the changes made to the document |
| `doc`    | `Node`                 | The document **after** the changes have been applied              |
| `before` | `Node`                 | The document **before** the changes have been applied             |

### ChangeTrackingStep

The `ChangeTrackingStep` is a class that represents a single change made to the document, based on ProseMirror's `ReplaceStep` class. It has the following property:

| Property      | Type          | Description                                             |
| ------------- | ------------- | ------------------------------------------------------- |
| `attribution` | `Attribution` | Metadata about the change, such as the user who made it |

### Types

Here is the full TypeScript definition for the `SnapshotCompare` extension:

```ts
declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    snapshotCompare: {
      /**
       * Given a change tracking transform, show the diff within the editor
       */
      showDiff: (tr: ChangeTrackingTransform, options?: { diffs?: DiffSet }) => ReturnType
      /**
       * Hide the diff and restore the previous content
       */
      hideDiff: () => ReturnType
      /**
       * Diffs two versions and renders the diff into the editor
       */
      compareVersions: <
        T extends Pick<Attribution, 'color'> & Record<string, any> = Pick<Attribution, 'color'> &
          Record<string, any>,
      >(options: {
        /**
         * The version to start the diff from
         */
        fromVersion: number
        /**
         * The version to end the diff at
         * If not provided, the latest snapshot will be used
         */
        toVersion?: number
        /**
         * Allows adding contextual data to each users changes
         */
        hydrateUserData?: (context: {
          /**
           * The type of event
           */
          type: 'added' | 'removed'
          /**
           * The userId that manipulated this content
           */
          userId: string | undefined
          /**
           * The yjs identifier of the content
           */
          id?: y.ID
        }) => T
        /**
         * If provided, allows customizing the behavior of rendering the diffs to the editor.
         * @note The default behavior would be to just display the diff immediately.
         */
        onCompare?: (
          context:
            | {
                error?: undefined
                /**
                 * The editor instance
                 */
                editor: Editor
                /**
                 * All of the changes as a transform with attribution metadata
                 */
                tr: ChangeTrackingTransform<Attribution & T>
                /**
                 * The changes represented as an array of diffs
                 */
                diffSet: DiffSet<Attribution & T>
              }
            | {
                error: Error
              },
        ) => void
        /**
         * Verbosely log the diffing process to help track down where things went wrong
         */
        enableDebugging?: boolean
      }) => ReturnType
    }
  }
}

export type SnapshotCompareOptions = {
  /**
   * The Tiptap provider instance. This is required for the extension to compute the diffs, but not to display them.
   * It is also possible to pass a TiptapCollabProvider instance.
   */
  provider: TiptapCollabProvider | null
  /**
   * This allows you to control mapping of a diff into a decoration to display the content of that diff
   */
  mapDiffToDecorations?: (options: {
    /**
     * The diff to map to a decoration
     */
    diff: Diff
    /**
     * The editor instance
     */
    editor: Editor
    /**
     * The change tracking transform
     */
    tr: ChangeTrackingTransform
    /**
     * The default implementation of how to map a diff to a decoration
     */
    defaultMapDiffToDecorations: typeof defaultMapDiffToDecorations
  }) => ReturnType<typeof defaultMapDiffToDecorations>
}

export type SnapshotCompareStorageInactive = {
  /**
   * Whether the diff view is currently active
   */
  isPreviewing: false
  /**
   * The content before the diff view was applied
   */
  previousContent: null
  /**
   * The change tracking transform that was applied
   * It is currently empty because the diff view is not active
   */
  diffs: DiffSet
  /**
   * The change tracking transform that was applied
   */
  tr: null
}

export type SnapshotCompareStorageActive = {
  /**
   * Whether the diff view is currently active
   */
  isPreviewing: true
  /**
   * The content before the diff view was applied
   */
  previousContent: JSONContent
  /**
   * The change tracking transform that was applied
   */
  diffs: DiffSet
  /**
   * The change tracking transform that was applied
   */
  tr: ChangeTrackingTransform
}

export type SnapshotCompareStorage = SnapshotCompareStorageInactive | SnapshotCompareStorageActive
```
