Skip to main content

Collaborative Editing

Tiptap provides first-class support for real-time collaborative editing through the Collaboration extension, which is powered by Yjs, a battle-tested CRDT (Conflict-free Replicated Data Type) framework.

Installation

First, install the required packages:
npm install @tiptap/extension-collaboration yjs

Basic Setup

The Collaboration extension requires a Yjs document to synchronize content between users.
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'

// Create a Yjs document
const ydoc = new Y.Doc()

const editor = new Editor({
  extensions: [
    StarterKit.configure({
      // Disable the default history extension
      // (collaboration comes with its own)
      history: false,
    }),
    Collaboration.configure({
      document: ydoc,
    }),
  ],
})

Understanding the Collaboration Extension

The Collaboration extension provides undo/redo functionality and real-time synchronization.
export interface CollaborationOptions {
  // An initialized Y.js document
  document?: Doc | null
  
  // Name of a Y.js fragment (for multiple fields in one document)
  field?: string
  
  // A raw Y.js fragment (alternative to document + field)
  fragment?: XmlFragment | null
  
  // The collaboration provider
  provider?: any | null
  
  // Callback when content is initially rendered
  onFirstRender?: () => void
  
  // Options for the Yjs sync plugin
  ySyncOptions?: YSyncOpts
  
  // Options for the Yjs undo plugin
  yUndoOptions?: YUndoOpts
}
Source: packages/extension-collaboration/src/collaboration.ts:41

Collaboration Commands

The extension adds undo/redo commands that work with collaborative editing.
// Undo recent changes
editor.commands.undo()

// Redo changes
editor.commands.redo()

// Check if undo is available
if (editor.can().undo()) {
  editor.commands.undo()
}

// Check if redo is available
if (editor.can().redo()) {
  editor.commands.redo()
}
Source: packages/extension-collaboration/src/collaboration.ts:121

Keyboard Shortcuts

The extension provides standard keyboard shortcuts.
// Undo
'Mod-z': () => this.editor.commands.undo()

// Redo
'Mod-y': () => this.editor.commands.redo()
'Shift-Mod-z': () => this.editor.commands.redo()
Source: packages/extension-collaboration/src/collaboration.ts:160

WebSocket Providers

To sync between multiple clients, you need a provider. Here are popular options:

y-websocket

npm install y-websocket

y-webrtc

For peer-to-peer connections without a server.
npm install y-webrtc

Collaboration Cursor

Show where other users are editing with the CollaborationCursor extension.
npm install @tiptap/extension-collaboration-cursor

Styling Collaboration Cursors

/* Collaboration cursor */
.collaboration-cursor__caret {
  border-left: 2px solid;
  border-color: currentColor;
  margin-left: -1px;
  margin-right: -1px;
  pointer-events: none;
  position: relative;
  word-break: normal;
}

/* Cursor label */
.collaboration-cursor__label {
  background-color: currentColor;
  border-radius: 0.25rem;
  color: white;
  font-size: 0.75rem;
  font-weight: 600;
  left: -1px;
  line-height: 1;
  padding: 0.125rem 0.375rem;
  position: absolute;
  top: -1.5rem;
  user-select: none;
  white-space: nowrap;
}

Multiple Fields

You can use multiple editor instances with the same Yjs document.
import * as Y from 'yjs'

const ydoc = new Y.Doc()

// Title editor
const titleEditor = new Editor({
  extensions: [
    StarterKit.configure({ history: false }),
    Collaboration.configure({
      document: ydoc,
      field: 'title',  // Use 'title' fragment
    }),
  ],
})

// Content editor
const contentEditor = new Editor({
  extensions: [
    StarterKit.configure({ history: false }),
    Collaboration.configure({
      document: ydoc,
      field: 'content',  // Use 'content' fragment
    }),
  ],
})
Source: packages/extension-collaboration/src/collaboration.ts:49

Offline Support

Yjs can work offline and sync when reconnected.
import { WebsocketProvider } from 'y-websocket'
import * as Y from 'yjs'

const ydoc = new Y.Doc()
const provider = new WebsocketProvider('ws://localhost:1234', 'my-doc', ydoc)

// Listen for connection status
provider.on('status', (event: { status: string }) => {
  if (event.status === 'connected') {
    console.log('Connected to server')
  } else if (event.status === 'disconnected') {
    console.log('Disconnected from server')
  }
})

// Listen for sync events
provider.on('sync', (isSynced: boolean) => {
  if (isSynced) {
    console.log('Document synced')
  }
})

Persistence

Persist collaborative documents to a database.
import { IndexeddbPersistence } from 'y-indexeddb'
import * as Y from 'yjs'

const ydoc = new Y.Doc()

// Persist to IndexedDB
const persistence = new IndexeddbPersistence('my-document', ydoc)

persistence.on('synced', () => {
  console.log('Document loaded from IndexedDB')
})

React Example

Complete React example with collaboration.
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import { WebsocketProvider } from 'y-websocket'
import * as Y from 'yjs'
import { useEffect, useState } from 'react'

export default function CollaborativeEditor({ room, user }) {
  const [provider, setProvider] = useState<WebsocketProvider | null>(null)
  
  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        history: false,
      }),
      Collaboration.configure({
        document: provider?.document,
      }),
      CollaborationCursor.configure({
        provider: provider,
        user: {
          name: user.name,
          color: user.color,
        },
      }),
    ],
  })
  
  useEffect(() => {
    const ydoc = new Y.Doc()
    const websocketProvider = new WebsocketProvider(
      'ws://localhost:1234',
      room,
      ydoc
    )
    
    setProvider(websocketProvider)
    
    return () => {
      websocketProvider?.destroy()
      ydoc?.destroy()
    }
  }, [room])
  
  if (!editor || !provider) {
    return <div>Loading...</div>
  }
  
  return (
    <div>
      <div className="collaboration-status">
        {provider.wsconnected ? 'Connected' : 'Disconnected'}
      </div>
      <EditorContent editor={editor} />
    </div>
  )
}

Conflict Resolution

Yjs automatically handles conflicts using CRDTs.
// User A types "Hello"
// User B types "World" at the same position
// Result: "HelloWorld" or "WorldHello" (deterministic)

// Yjs ensures:
// 1. All clients converge to the same state
// 2. No data loss
// 3. Causal consistency
// 4. Intention preservation

Performance Optimization

import Collaboration from '@tiptap/extension-collaboration'

const editor = new Editor({
  extensions: [
    Collaboration.configure({
      document: ydoc,
      
      // Undo/redo options
      yUndoOptions: {
        // Number of milliseconds to group changes
        trackedOrigins: [],
        // Capture timeout in ms
        captureTimeout: 500,
      },
      
      // Sync options
      ySyncOptions: {
        // Custom colors for collaboration cursors
        colors: [
          { light: '#6366f1', dark: '#4f46e5' },
          { light: '#ec4899', dark: '#db2777' },
          { light: '#14b8a6', dark: '#0d9488' },
        ],
      },
    }),
  ],
})
Source: packages/extension-collaboration/src/collaboration.ts:73

Disabling Collaboration

Temporarily disable collaboration to prevent syncing.
// Check if disabled
if (editor.storage.collaboration.isDisabled) {
  console.log('Collaboration is disabled')
}

// The extension can disable itself if content errors occur
editor.on('contentError', ({ disableCollaboration }) => {
  // This will disable collaboration and prevent syncing
  disableCollaboration()
})
Source: packages/extension-collaboration/src/collaboration.ts:12
The Collaboration extension is not compatible with the History extension. The collaboration extension provides its own undo/redo functionality.

Next Steps

TypeScript

Add type safety to your collaborative editor

Styling

Style collaboration cursors and indicators

Build docs developers (and LLMs) love