The Collaboration extension enables real-time collaborative editing in Tiptap using Yjs. It synchronizes content across multiple users and includes built-in undo/redo functionality that works seamlessly in collaborative environments.
The Collaboration extension comes with its own history implementation and is not compatible with the @tiptap/extension-undo-redo extension. Make sure to remove the UndoRedo extension if you’re using Collaboration.
Installation
npm install @tiptap/extension-collaboration yjs
You’ll also need a Yjs provider to sync data between clients. Popular options include:
npm install y-websocket # WebSocket provider
npm install y-webrtc # WebRTC provider
Basic Usage
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
// Create a Y.js document
const ydoc = new Y.Doc()
// Create a provider for syncing (WebSocket example)
const provider = new WebsocketProvider(
'ws://localhost:1234',
'document-name',
ydoc
)
const editor = new Editor({
extensions: [
StarterKit.configure({
// Disable the default history extension
history: false,
}),
Collaboration.configure({
document: ydoc,
}),
],
})
Configuration Options
An initialized Y.js document. This is the shared data structure that will be synchronized across all clients.Example:import * as Y from 'yjs'
const ydoc = new Y.Doc()
Collaboration.configure({
document: ydoc,
})
Name of the Y.js fragment to use. You can use different fields to sync multiple editors with one Y.js document.Default: 'default'Collaboration.configure({
document: ydoc,
field: 'body', // Store editor content in 'body' field
})
// You could have another editor using the same document
Collaboration.configure({
document: ydoc,
field: 'sidebar', // Store different content in 'sidebar' field
})
A raw Y.js fragment. Use this instead of document and field if you want direct control over the fragment.const ydoc = new Y.Doc()
const fragment = ydoc.getXmlFragment('custom-fragment')
Collaboration.configure({
fragment: fragment,
})
The collaboration provider instance (e.g., WebsocketProvider, WebrtcProvider). While not required by the extension itself, storing it here makes it accessible via the extension.Default: nullimport { WebsocketProvider } from 'y-websocket'
const provider = new WebsocketProvider('ws://localhost:1234', 'doc', ydoc)
Collaboration.configure({
document: ydoc,
provider: provider,
})
Callback fired when the content from Yjs is initially rendered to Tiptap. Useful for showing a loading state until the document is ready.Collaboration.configure({
document: ydoc,
onFirstRender: () => {
console.log('Document loaded and rendered')
hideLoadingSpinner()
},
})
Options passed to the Y.js sync plugin. See y-prosemirror documentation for available options.Collaboration.configure({
document: ydoc,
ySyncOptions: {
colors: [
{ light: '#ecd44433', dark: '#ecd444' },
{ light: '#ee635233', dark: '#ee6352' },
],
},
})
Options passed to the Y.js undo plugin.Collaboration.configure({
document: ydoc,
yUndoOptions: {
trackedOrigins: new Set([ydoc.clientID]),
},
})
Commands
Undo recent changes.Keyboard shortcut: Cmd/Ctrl + Z
Reapply reverted changes.Keyboard shortcuts: Cmd/Ctrl + Shift + Z or Cmd/Ctrl + Y
Storage
Whether collaboration is currently disabled. This is automatically set when collaboration encounters a content error.// Check if collaboration is disabled
if (editor.storage.collaboration.isDisabled) {
console.log('Collaboration has been disabled')
}
Advanced Examples
Complete Collaborative Editor
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const ydoc = new Y.Doc()
const provider = new WebsocketProvider(
'ws://localhost:1234',
'my-document',
ydoc
)
const editor = new Editor({
extensions: [
StarterKit.configure({
history: false, // Important: disable default history
}),
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider: provider,
user: {
name: 'John Doe',
color: '#f783ac',
},
}),
],
})
Multiple Editors with One Document
const ydoc = new Y.Doc()
const provider = new WebsocketProvider('ws://localhost:1234', 'doc', ydoc)
// Main editor
const mainEditor = new Editor({
element: document.querySelector('#main-editor'),
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({
document: ydoc,
field: 'main',
}),
],
})
// Sidebar editor
const sidebarEditor = new Editor({
element: document.querySelector('#sidebar-editor'),
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({
document: ydoc,
field: 'sidebar',
}),
],
})
Loading State
let isDocumentLoaded = false
const editor = new Editor({
editable: false, // Start in read-only mode
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({
document: ydoc,
onFirstRender: () => {
isDocumentLoaded = true
editor.setEditable(true)
hideLoadingSpinner()
},
}),
],
})
showLoadingSpinner()
Content Error Handling
const editor = new Editor({
enableContentCheck: true,
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({
document: ydoc,
}),
],
onContentError: ({ editor, error, disableCollaboration }) => {
console.error('Content validation error:', error)
// Option 1: Disable collaboration to prevent further issues
disableCollaboration()
// Option 2: Show error to user
showErrorNotification(
'Document contains invalid content. Collaboration has been disabled.'
)
// Option 3: Attempt recovery
editor.commands.setContent('<p>Content was reset due to an error</p>')
},
})
Custom Provider Setup
import { WebrtcProvider } from 'y-webrtc'
const ydoc = new Y.Doc()
// WebRTC provider for peer-to-peer collaboration
const provider = new WebrtcProvider('my-document', ydoc, {
signaling: ['wss://signaling.example.com'],
password: 'optional-room-password',
})
const editor = new Editor({
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({
document: ydoc,
provider: provider,
}),
],
})
// Clean up on unmount
window.addEventListener('beforeunload', () => {
provider.destroy()
ydoc.destroy()
})
Custom Undo/Redo Tracking
Collaboration.configure({
document: ydoc,
yUndoOptions: {
// Only track changes from this user
trackedOrigins: new Set([ydoc.clientID]),
// Capture timeout in ms
captureTimeout: 500,
},
})
Keyboard Shortcuts
| Shortcut | Command | Description |
|---|
Cmd/Ctrl + Z | undo | Undo the last change |
Cmd/Ctrl + Shift + Z | redo | Redo the last undone change |
Cmd/Ctrl + Y | redo | Redo the last undone change |
Storage Access
// Check if collaboration is disabled
const isDisabled = editor.storage.collaboration.isDisabled
// Access the Y.js document
const ydoc = editor.extensionManager.extensions
.find(ext => ext.name === 'collaboration')
.options.document
Utilities
The Collaboration extension adds utility methods to the editor:
// Get updated position after transaction
const newPosition = editor.utils.getUpdatedPosition(oldPosition, transaction)
// Create a mappable position that tracks across transactions
const mappablePos = editor.utils.createMappablePosition(position)
Source Code
View the source code on GitHub: