This enables live cursors and collaborative editing out of the box. This is framework agnostic and can be used with any Tiptap editor.

Step 1: Install Dependencies

First, install the required packages:

npm install @veltdev/tiptap-crdt @tiptap/react @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor

Step 2: Setup Velt

Learn more

Step 3: Import Required Components

Import all necessary components and hooks:

import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useVeltClient, useVeltEventCallback } from '@veltdev/react';
import { VeltTipTapStore, createVeltTipTapStore } from '@veltdev/tiptap-crdt';
import { User } from '@veltdev/types';
import React, { useEffect, useRef, useState } from 'react';

Step 4: Setup Component State

Create the necessary state variables and refs:

const CollaborativeEditor: React.FC = () => {
  // Store reference to hold the VeltTipTap store instance
  const veltTiptapStoreRef = useRef<VeltTipTapStore | null>(null);

  // Get Velt client and user from Velt hooks. Velt User will be used to generate the live text cursors.
  const { client } = useVeltClient();
  const veltUser = useVeltEventCallback('userUpdate');

  // Track when the store is ready for editor initialization
  const [veltTiptapStoreReady, setVeltTiptapStoreReady] = useState(false);

Step 5: Initialize the VeltTipTap Store

  • Set up the store initialization.
  • Pass the editorId and Velt client to the Velt Tiptap store. When multiple editors are used, each editor should have a unique editorId.
  • Set the local state to track when the store is ready.
  • Add cleanup function to destroy the store when the component unmounts.
  // Initialize the VeltTipTap store when client and user are available
  useEffect(() => {
    if (!veltUser || !client) return;

    initializeStore();

    // Cleanup function to destroy store when component unmounts
    return () => {
      if (veltTiptapStoreRef.current) {
        veltTiptapStoreRef.current.destroy();
      }
    };
  }, [client, veltUser]);

  const initializeStore = async () => {
    // Create the VeltTipTap store with unique editor ID and Velt client
    const veltTiptapStore = await createVeltTipTapStore({ 
      editorId: 'my-collaborative-editor', // Unique identifier for this editor
      veltClient: client! 
    });
    
    veltTiptapStoreRef.current = veltTiptapStore;
    setVeltTiptapStoreReady(true);
  }

Step 6: Configure the TipTap Editor

  • Set up the editor with collaboration extensions.
  • Add the collaboration extension from the VeltTipTap store when the store is ready.
  • Configure cursor tracking with the same provider and Velt User.
  • Disable default history to prevent conflicts.
  // Initialize TipTap editor with collaboration extensions
  const editor = useEditor({
    extensions: [
      // Basic TipTap extensions
      StarterKit.configure({
        history: false, // Disable default history to prevent conflicts
      }),
      // Add collaboration extensions only when store is ready
      ...(
        veltTiptapStoreRef.current ?
          [
            // Get the collaboration extension from the VeltTipTap store
            veltTiptapStoreRef.current.getCollabExtension(),
            // Configure cursor tracking with the same provider
            CollaborationCursor.configure({
              provider: veltTiptapStoreRef.current.getStore().getProvider(),
              user: veltUser as User,
            }),
          ]
          : []
      ),
    ],
    // Start with empty content to avoid conflicts with YJS initialization
    content: ''
  }, [veltTiptapStoreReady, veltUser]); // Re-initialize when store is ready or user changes

Step 7: Render the Editor

  return (
    <div className="editor-container">
      <div className="editor-header">
        Collaborative Editor - {veltUser?.name ? `Editing as ${veltUser.name}` : 'Please login to start editing'}
      </div>
      <div className="editor-content">
        <EditorContent editor={editor} />
      </div>
      <div className="status">
        {veltTiptapStoreReady ? 'Connected to collaborative session' : 'Connecting to collaborative session...'}
      </div>
    </div>
  );
};

export default CollaborativeEditor;

Complete Example

Here’s the full implementation:

import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useVeltClient, useVeltEventCallback } from '@veltdev/react';
import { VeltTipTapStore, createVeltTipTapStore } from '@veltdev/tiptap-crdt';
import { User } from '@veltdev/types';
import React, { useEffect, useRef, useState } from 'react';

const CollaborativeEditor: React.FC = () => {
  // Store reference to hold the VeltTipTap store instance
  const veltTiptapStoreRef = useRef<VeltTipTapStore | null>(null);

  // Get Velt client and user from Velt hooks. This assumes you have already initialized the Velt client.
  const { client } = useVeltClient();
  const veltUser = useVeltEventCallback('userUpdate');

  // Track when the store is ready for editor initialization
  const [veltTiptapStoreReady, setVeltTiptapStoreReady] = useState(false);

  // Initialize the VeltTipTap store when client and user are available
  useEffect(() => {
    if (!veltUser || !client) return;

    initializeStore();

    // Cleanup function to destroy store when component unmounts
    return () => {
      if (veltTiptapStoreRef.current) {
        veltTiptapStoreRef.current.destroy();
      }
    };
  }, [client, veltUser]);

  const initializeStore = async () => {
    // Create the VeltTipTap store with unique editor ID and Velt client
    const veltTiptapStore = await createVeltTipTapStore({ 
      editorId: 'my-collaborative-editor', // Unique identifier for this editor
      veltClient: client! 
    });
    
    veltTiptapStoreRef.current = veltTiptapStore;
    setVeltTiptapStoreReady(true);
  }

  // Initialize TipTap editor with collaboration extensions
  const editor = useEditor({
    extensions: [
      // Basic TipTap extensions
      StarterKit.configure({
        history: false, // Disable default history to use Collaboration's history management
      }),
      // Add collaboration extensions only when store is ready
      ...(
        veltTiptapStoreRef.current ?
          [
            // Get the collaboration extension from the VeltTipTap store
            veltTiptapStoreRef.current.getCollabExtension(),
            // Configure cursor tracking with the same provider
            CollaborationCursor.configure({
              provider: veltTiptapStoreRef.current.getStore().getProvider(),
              user: veltUser as User,
            }),
          ]
          : []
      ),
    ],
    // Start with empty content to avoid conflicts with YJS initialization
    content: ''
  }, [veltTiptapStoreReady, veltUser]); // Re-initialize when store is ready or user changes

  return (
    <div className="editor-container">
      <div className="editor-header">
        Collaborative Editor - {veltUser?.name ? `Editing as ${veltUser.name}` : 'Please login to start editing'}
      </div>
      <div className="editor-content">
        <EditorContent editor={editor} />
      </div>
      <div className="status">
        {veltTiptapStoreReady ? 'Connected to collaborative session' : 'Connecting to collaborative session...'}
      </div>
    </div>
  );
};

export default CollaborativeEditor;

Key Points

  • Unique Editor ID: Each editor instance needs a unique editorId to maintain separate collaborative sessions
  • Store Lifecycle: The VeltTipTap store is created asynchronously and must be properly cleaned up
  • Extension Order: Collaboration extensions are only added after the store is ready
  • History Management: Disable TipTap’s default history to prevent conflicts
  • User Authentication: Ensure users are authenticated before initializing the collaborative session