Skip to main content
The @veltdev/quill-crdt-react and @veltdev/quill-crdt libraries connect Quill 2 to a Velt CRDT text Store backed by Yjs. Rich text is represented as Quill Delta operations on one Y.Text, the base manager owns the y-quill binding and Yjs undo manager, and quill-cursors renders awareness cursors.

Prerequisites

  • Node.js (v14 or higher)
  • Quill 2
  • A Velt account with an API key (sign up)
  • React (v18 or higher) when using the React wrapper
  • Optional: TypeScript for type safety

Setup

Step 1: Install Dependencies

The verified base integration uses quill@2.0.3, y-quill@1.0.0, and quill-cursors@4.3.0. Resolve one compatible copy of yjs across @veltdev/crdt, @veltdev/quill-crdt, and y-quill.

Step 2: Load Styles and Register Cursors

Import the stylesheet for the selected Quill theme:
QuillCrdtEditor dynamically loads Quill and registers quill-cursors in the browser. Register it yourself before creating a custom Quill instance:
The cursor module must be registered before new Quill(). Without it, document sync can still work, but remote cursor labels and selections will not render.

Step 3: Configure the Quill Editor

The drop-in component creates Quill internally. For the hook path, define the options now and construct Quill in a client-only effect only after the Velt document scope in Step 4 is ready:
The source integration keeps Quill history local-only with userOnly: true. Expose the manager’s Yjs-aware undo() and redo() methods as the user-facing collaborative history controls.

Step 4: Setup Velt

Initialize Velt, authenticate a user, and set stable document identity before creating collaboration. Follow the Velt Setup Docs for the production token endpoint used by authProvider.
Every collaborator editing the same rich-text document must use the same Velt document ID and Quill editorId.

Step 5: Initialize Collaboration

QuillCrdtEditor registers cursors, creates Quill, and manages the collaboration lifecycle:

React Context Provider (Optional)

React applications can share one hook-managed collaboration value with descendant controls:
CollaborationProvider is an alias of QuillCrdtProvider. Use one provider or one hook for an editor, not both.

Step 6: Add Editor and Cursor Styles

The Snow stylesheet supplies the editor theme. quill-cursors supplies cursor geometry and labels. Do not hide .ql-cursor or .ql-cursor-selection in application CSS.

Step 7: Initial Content and Reset Behavior

initialContent accepts plain text or a Quill Delta:
It is applied only when the shared document is new. A later client receives the persisted Delta instead of replacing it. Use an explicit reset only for a destructive action:
forceResetInitialContent and forceReset() replace shared rich-text content for every collaborator and clear collaborative undo history.

Step 8: Monitor Status and Remote Selections

The component render function and hook expose reactive status, synced, remoteSelections, and stats:
The y-quill binding publishes normal editor selection changes and quill-cursors renders them. If custom selection UI changes the range outside that normal flow, publish explicitly:
Selections are ephemeral awareness state and are not persisted in the rich-text document.

Step 9: Work with Text and Delta Content

Use highlightRange() for a persistent Delta background attribute. This differs from a remote awareness selection:

Step 10: Configure Collaborative Undo and Redo

Use the manager’s Yjs-aware history methods:
undoCaptureTimeout controls how quickly adjacent local operations are grouped. The manager-scoped Y.UndoManager is the collaborative history source; do not wire your user-facing controls to a separate Quill history stack.

Step 11: Save and Restore Versions

Saving flushes current Quill contents first. Restoring replaces the shared Delta and clears stale undo history.

Step 12: Subscribe to CRDT Events

Step 13: Attach or Replace an Editor (Alternative)

Initialize the Store without Quill only when it must outlive the editor:
This is an alternative to passing editor to createCollaboration(). Detaching destroys the active y-quill binding but leaves the Store running.

Step 14: Custom Encryption (Optional)

See setEncryptionProvider() and VeltEncryptionProvider.

Step 15: Clean Up

useCollaboration() unsubscribes and destroys its manager on unmount. QuillCrdtEditor also clears the host that it created. When an app owns the custom Quill host, explicitly release collaboration before removing its DOM:
The hook’s later unmount cleanup is idempotent, so the explicit destroy() safely preserves the required order.

Step 16: Client-only Rendering

Quill requires browser APIs. The drop-in React component dynamically imports Quill, but the route containing it must still be a client component in an SSR framework:

Notes

  • Rich-text model: Delta text, attributes, and embeds are stored on one shared Y.Text.
  • Binding ownership: @veltdev/quill-crdt owns the only y-quill binding, provider, awareness instance, and Yjs undo manager.
  • Cursor module: Register quill-cursors before a custom new Quill() call.
  • Persistent versus transient state: Delta background highlights persist; awareness selections do not.
  • Single Yjs copy: Run npm ls yjs (or the package-manager equivalent) and deduplicate if more than one version appears.
  • Advanced escape hatches: Use getDoc(), getYText(), getProvider(), and getAwareness() only for diagnostics or advanced integrations. Do not create a second Y.Doc, Y.Text, provider, or direct QuillBinding.
  • Production authentication: Use a server-backed React authProvider and never expose privileged server tokens in browser code.

Testing and Debugging

  1. Open the same Velt document and Quill editorId as two different authenticated users in separate browser profiles.
  2. Edit text, formatting, lists, links, embeds, and highlights concurrently.
  3. Verify remote cursor labels, selected ranges, manager undo/redo, versions, reconnection, and persistence.
  4. Test with the production modules and formats allowlist.
  5. Run npm ls yjs and confirm the application resolves one Yjs copy.
Common issues:
  • Cursors do not render: register quill-cursors before editor creation, enable the cursors module, and keep cursor CSS visible.
  • Formatting disappears: include that format in Quill’s formats allowlist.
  • Editor remains loading: confirm Velt initialization, authentication, and the captured Quill instance, or use initializeWithoutEditor.
  • Duplicate Yjs warning: deduplicate yjs, @veltdev/crdt, and y-quill.
Use the VeltCrdtStoreMap debugging interface to inspect shared Delta state and provider status.

Complete Example

Complete App.tsx
Complete styles.css

How It Works

  1. The app loads the theme and registers quill-cursors, initializes Velt, authenticates the user, sets stable document identity, and then creates Quill before creating collaboration.
  2. The manager creates one Velt text Store, shared Y.Text, provider, awareness instance, Yjs UndoManager, and y-quill binding.
  3. Quill Delta changes and remote Yjs transactions flow through that single binding.
  4. The binding publishes selection positions through awareness, and quill-cursors renders remote cursors and labels.
  5. Manager undo/redo tracks collaborative Yjs transactions rather than a standalone Quill history stack.
  6. Cleanup unsubscribes callbacks, destroys the binding and manager, then removes Quill’s DOM.

APIs

React: QuillCrdtEditor

React: useCollaboration()

CollaborationManager Methods

Exported Types

  • QuillEditorLike, QuillDelta, QuillRange, QuillCursorData, RemoteSelection
  • QuillModules, QuillFormats, QuillCursorsModule, BindEditorOptions
  • CollaborationConfig, CollaborationPrimitives, CollaborationStats, SyncStatus, Unsubscribe, Version