> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Quill Editor

> Setup Multiplayer Editing for Quill Editor.

The `@veltdev/quill-crdt-react` and `@veltdev/quill-crdt` libraries connect Quill 2 to a Velt CRDT text Store backed by [Yjs](https://docs.yjs.dev/). 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](https://console.velt.dev/))
* React (v18 or higher) when using the React wrapper
* Optional: TypeScript for type safety

## Setup

### Step 1: Install Dependencies

> [tab] React / Next.js
>
> ```bash theme={null}
> npm install @veltdev/quill-crdt-react @veltdev/quill-crdt @veltdev/react @veltdev/types @veltdev/crdt react react-dom quill quill-cursors y-quill yjs y-protocols
> ```

> [tab] Other Frameworks
>
> ```bash theme={null}
> npm install @veltdev/quill-crdt @veltdev/crdt @veltdev/client quill quill-cursors y-quill yjs y-protocols
> ```

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:

```ts theme={null}
import 'quill/dist/quill.snow.css';
```

> [tab] React / Next.js
>
> `QuillCrdtEditor` dynamically loads Quill and registers `quill-cursors` in the browser. Register it yourself before creating a custom Quill instance:
>
> ```tsx theme={null}
> import Quill from 'quill';
> import QuillCursors from 'quill-cursors';
> import 'quill/dist/quill.snow.css';
>
> Quill.register('modules/cursors', QuillCursors);
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> import Quill from 'quill';
> import QuillCursors from 'quill-cursors';
> import 'quill/dist/quill.snow.css';
>
> Quill.register('modules/cursors', QuillCursors);
> ```

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

```html theme={null}
<div id="quill-editor" class="quill-host"></div>
```

> [tab] React / Next.js
>
> 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:
>
> ```tsx theme={null}
> const quillOptions = {
>   theme: 'snow',
>   placeholder: 'Start writing...',
>   modules: {
>     cursors: { transformOnTextChange: true },
>     history: { userOnly: true },
>     toolbar: [
>       ['bold', 'italic', 'underline'],
>       [{ list: 'ordered' }, { list: 'bullet' }],
>       ['link', 'clean'],
>     ],
>   },
> };
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const quillOptions = {
>   theme: 'snow',
>   placeholder: 'Start writing...',
>   modules: {
>     cursors: { transformOnTextChange: true },
>     history: { userOnly: true },
>     toolbar: [
>       ['bold', 'italic', 'underline'],
>       [{ list: 'ordered' }, { list: 'bullet' }],
>       ['link', 'clean'],
>     ],
>   },
> };
> ```

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](https://velt.dev/docs/get-started/quickstart.md) for the production token endpoint used by `authProvider`.

> [tab] React / Next.js
>
> ```tsx theme={null}
> import { useEffect, useState } from 'react';
> import {
>   VeltProvider,
>   useCurrentUser,
>   useSetDocuments,
> } from '@veltdev/react';
>
> const DOCUMENT_ID = 'shared-quill-doc';
> const USER = {
>   userId: 'ada',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'my-org-id',
> };
>
> const authProvider = {
>   user: USER,
>   retryConfig: { retryCount: 3, retryDelay: 1000 },
>   generateToken: async () => {
>     const response = await fetch('/api/velt/token', {
>       method: 'POST',
>       headers: { 'Content-Type': 'application/json' },
>       body: JSON.stringify(USER),
>     });
>     const { token } = await response.json();
>     return token;
>   },
> };
>
> function DocumentScope() {
>   const user = useCurrentUser();
>   const { setDocuments } = useSetDocuments();
>   const [documentReady, setDocumentReady] = useState(false);
>
>   useEffect(() => {
>     if (!user) return;
>     setDocuments([{
>       id: DOCUMENT_ID,
>       metadata: { documentName: 'Shared Quill document' },
>     }]);
>     setDocumentReady(true);
>   }, [user, setDocuments]);
>
>   return user && documentReady
>     ? <CollaborativeEditor />
>     : <p>Preparing collaboration...</p>;
> }
>
> <VeltProvider apiKey="YOUR_VELT_API_KEY" authProvider={authProvider}>
>   <DocumentScope />
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> import { initVelt } from '@veltdev/client';
>
> const DOCUMENT_ID = 'shared-quill-doc';
> const client = await initVelt('YOUR_VELT_API_KEY');
>
> await client.identify({
>   userId: 'ada',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'my-org-id',
> });
> await client.setDocument(DOCUMENT_ID, {
>   documentName: 'Shared Quill document',
> });
>
> let resolveVeltReady!: () => void;
> const veltReady = new Promise<void>((resolve) => {
>   resolveVeltReady = resolve;
> });
> const initSubscription = client.getVeltInitState().subscribe((isReady) => {
>   if (isReady) resolveVeltReady();
> });
> await veltReady;
> initSubscription.unsubscribe();
> ```

Every collaborator editing the same rich-text document must use the same Velt document ID and Quill `editorId`.

### Step 5: Initialize Collaboration

> [tab] React Component
>
> `QuillCrdtEditor` registers cursors, creates Quill, and manages the collaboration lifecycle:
>
> ```tsx theme={null}
> import { QuillCrdtEditor } from '@veltdev/quill-crdt-react';
> import 'quill/dist/quill.snow.css';
>
> function CollaborativeEditor() {
>   return (
>     <QuillCrdtEditor
>       documentId="shared-quill-doc"
>       theme="snow"
>       className="quill-host"
>       modules={{
>         cursors: { transformOnTextChange: true },
>         history: { userOnly: true },
>         toolbar: [['bold', 'italic'], [{ list: 'bullet' }]],
>       }}
>       initialContent={{
>         ops: [
>           { insert: 'Collaborative Quill document', attributes: { bold: true } },
>           { insert: '\n' },
>         ],
>       }}
>       cursorData={{
>         name: 'Ada Lovelace',
>         email: 'ada@example.com',
>         color: '#0f766e',
>         colorLight: '#0f766e33',
>       }}
>       onError={(error) => console.error('Quill collaboration:', error)}
>     />
>   );
> }
> ```

> [tab] React Hook
>
> Use `editorRef` when your application creates Quill:
>
> ```tsx theme={null}
> import { useEffect, useRef } from 'react';
> import Quill from 'quill';
> import { useCollaboration } from '@veltdev/quill-crdt-react';
>
> function CollaborativeEditor() {
>   const hostRef = useRef<HTMLDivElement>(null);
>   const collaboration = useCollaboration({
>     editorId: 'shared-quill-doc',
>     initialContent: 'Start writing together.\n',
>     cursorData: { name: 'Ada', color: '#0f766e' },
>     onError: (error) => console.error('Quill collaboration:', error),
>   });
>
>   useEffect(() => {
>     const host = hostRef.current;
>     if (!host) return;
>
>     const editor = new Quill(host, {
>       theme: 'snow',
>       modules: {
>         cursors: { transformOnTextChange: true },
>         history: { userOnly: true },
>       },
>     });
>     collaboration.editorRef(editor);
>
>     return () => {
>       collaboration.destroy();
>       collaboration.editorRef(null);
>       host.replaceChildren();
>     };
>   }, [collaboration.destroy, collaboration.editorRef]);
>
>   return <div ref={hostRef} className="quill-host" />;
> }
> ```
>
> The hook waits for Velt initialization, an authenticated user, and the captured editor. It does not explicitly wait for document context, so set the document before mounting this component.

> [tab] Other Frameworks
>
> ```ts theme={null}
> import { createCollaboration } from '@veltdev/quill-crdt';
>
> const host = document.querySelector<HTMLElement>('#quill-editor')!;
> const editor = new Quill(host, quillOptions);
>
> const manager = await createCollaboration({
>   editorId: DOCUMENT_ID,
>   veltClient: client,
>   editor,
>   initialContent: {
>     ops: [
>       { insert: 'Collaborative Quill document', attributes: { bold: true } },
>       { insert: '\n' },
>     ],
>   },
>   cursorData: {
>     name: 'Ada Lovelace',
>     email: 'ada@example.com',
>     color: '#0f766e',
>     colorLight: '#0f766e33',
>   },
>   onError: (error) => console.error('Quill collaboration:', error),
> });
> ```
>
> Passing `editor` creates the `y-quill` binding. Do not also call `attachEditor(editor)`.

#### React Context Provider (Optional)

React applications can share one hook-managed collaboration value with descendant controls:

```tsx theme={null}
import {
  QuillCrdtProvider,
  useCollaborationContext,
} from '@veltdev/quill-crdt-react';

function Toolbar() {
  const collaboration = useCollaborationContext();
  if (!collaboration) return null;
  return <button onClick={() => collaboration.saveVersion('Checkpoint')}>Save</button>;
}

<QuillCrdtProvider
  config={{ editorId: 'shared-quill-doc', editor }}
>
  <Toolbar />
</QuillCrdtProvider>
```

`CollaborationProvider` is an alias of `QuillCrdtProvider`. Use one provider or one hook for an editor, not both.

### Step 6: Add Editor and Cursor Styles

```css theme={null}
.quill-host {
  width: 100%;
  min-height: 360px;
}

.quill-host .ql-editor {
  min-height: 320px;
}

.quill-host .ql-cursor,
.quill-host .ql-cursor-selection {
  visibility: visible;
  opacity: 1;
}
```

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:

```ts theme={null}
const initialContent = {
  ops: [
    { insert: 'Quarterly plan', attributes: { header: 2 } },
    { insert: '\n' },
  ],
};
```

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:

```ts theme={null}
await manager.forceReset({
  ops: [{ insert: 'Reset document\n' }],
});
```

> [!WARNING]
>
> `forceResetInitialContent` and `forceReset()` replace shared rich-text content for every collaborator and clear collaborative undo history.

### Step 8: Monitor Status and Remote Selections

> [tab] React / Next.js
>
> The component render function and hook expose reactive `status`, `synced`, `remoteSelections`, and `stats`:
>
> ```tsx theme={null}
> <QuillCrdtEditor documentId="shared-quill-doc">
>   {(collaboration) => (
>     <p>
>       {collaboration.status} · {collaboration.synced ? 'Synced' : 'Syncing'}
>       {' · '}{collaboration.remoteSelections.length} remote selections
>     </p>
>   )}
> </QuillCrdtEditor>
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const offStatus = manager.onStatusChange((status) => {
>   document.querySelector('#status')!.textContent = status;
> });
> const offSynced = manager.onSynced((synced) => {
>   document.querySelector('#synced')!.textContent = synced ? 'Yes' : 'No';
> });
> const offSelections = manager.onRemoteSelectionsChange((selections) => {
>   console.log('Remote selections:', selections);
> });
> ```

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:

```ts theme={null}
manager.publishCurrentSelection();
manager.setLocalSelection({ index: 10, length: 4 });
```

Selections are ephemeral awareness state and are not persisted in the rich-text document.

### Step 9: Work with Text and Delta Content

```ts theme={null}
const text = manager.getValue();
const delta = manager.getContents();

manager.setValue('Plain text replacement', 'toolbar');
manager.setContents({
  ops: [{ insert: 'Rich text', attributes: { bold: true } }, { insert: '\n' }],
});
manager.updateContents({ ops: [{ retain: 4 }, { insert: ' collaborative' }] });

manager.insertText(0, 'Hello ', { bold: true });
manager.deleteText(0, 6);
manager.formatText(0, 5, { italic: true });
manager.insertEmbed(0, 'image', imageUrl);
```

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

```ts theme={null}
manager.highlightRange(0, 12, '#fef08a');
manager.clearHighlight(0, 12);
```

### Step 10: Configure Collaborative Undo and Redo

Use the manager's Yjs-aware history methods:

```ts theme={null}
manager.undo();
manager.redo();
manager.clearUndoHistory();
```

`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

```ts theme={null}
const versionId = await manager.saveVersion('Reviewed draft');
const versions = await manager.getVersions();
await manager.restoreVersion(versionId);
```

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

### Step 12: Subscribe to CRDT Events

> [tab] React / Next.js
>
> ```tsx theme={null}
> import { useVeltClient } from '@veltdev/react';
>
> const { client } = useVeltClient();
>
> useEffect(() => {
>   if (!client) return;
>   const subscription = client.getCrdtElement()
>     .on('updateData')
>     .subscribe((event) => console.log('CRDT update:', event));
>   return () => subscription.unsubscribe();
> }, [client]);
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const crdtSubscription = client.getCrdtElement()
>   .on('updateData')
>   .subscribe((event) => console.log('CRDT update:', event));
> ```

### Step 13: Attach or Replace an Editor (Alternative)

Initialize the Store without Quill only when it must outlive the editor:

> [tab] React / Next.js
>
> ```tsx theme={null}
> const collaboration = useCollaboration({
>   editorId: 'shared-quill-doc',
>   initializeWithoutEditor: true,
> });
>
> collaboration.attachEditor(editor);
> collaboration.detachEditor();
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const manager = await createCollaboration({
>   editorId: DOCUMENT_ID,
>   veltClient: client,
> });
>
> manager.attachEditor(editor);
> manager.detachEditor();
> ```

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)

> [tab] React / Next.js
>
> ```tsx theme={null}
> <VeltProvider
>   apiKey="YOUR_VELT_API_KEY"
>   authProvider={authProvider}
>   encryptionProvider={encryptionProvider}
> >
>   <DocumentScope />
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> client.setEncryptionProvider(encryptionProvider);
> ```

See [setEncryptionProvider()](https://velt.dev/docs/api-reference/sdk/api/api-methods.md#setencryptionprovider-encryptionprovider) and [VeltEncryptionProvider](https://velt.dev/docs/api-reference/sdk/models/data-models.md#veltencryptionprovider).

### Step 15: Clean Up

> [tab] React / Next.js
>
> `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:
>
> ```tsx theme={null}
> useEffect(() => {
>   const host = hostRef.current;
>   return () => {
>     collaboration.destroy();
>     collaboration.editorRef(null);
>     host?.replaceChildren();
>   };
> }, [collaboration.destroy, collaboration.editorRef]);
> ```
>
> The hook's later unmount cleanup is idempotent, so the explicit `destroy()` safely preserves the required order.

> [tab] Other Frameworks
>
> Unsubscribe first, destroy the manager and its `y-quill` binding, then remove Quill's DOM:
>
> ```ts theme={null}
> offStatus();
> offSynced();
> offSelections();
> crdtSubscription.unsubscribe();
> manager.destroy();
> host.replaceChildren();
> ```

### 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:

```tsx theme={null}
'use client';

import { QuillCrdtEditor } from '@veltdev/quill-crdt-react';
```

## 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`.

> [!TIP]
>
> Use the [VeltCrdtStoreMap debugging interface](https://velt.dev/docs/realtime-collaboration/crdt/setup/core.md#veltcrdtstoremap) to inspect shared Delta state and provider status.

## Complete Example

> [tab] React / Next.js
>
> ```tsx Complete App.tsx expandable lines theme={null}
> import { useEffect, useMemo, useState } from 'react';
> import {
>   VeltProvider,
>   useCurrentUser,
>   useSetDocuments,
> } from '@veltdev/react';
> import { QuillCrdtEditor } from '@veltdev/quill-crdt-react';
> import 'quill/dist/quill.snow.css';
> import './styles.css';
>
> const DOCUMENT_ID = 'shared-quill-doc';
> const USER = {
>   userId: 'ada',
>   organizationId: 'my-org-id',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
> };
>
> function CollaborativeEditor() {
>   return (
>     <main>
>       <QuillCrdtEditor
>         documentId={DOCUMENT_ID}
>         theme="snow"
>         className="quill-host"
>         initialContent={{
>           ops: [
>             { insert: 'Collaborative Quill document', attributes: { bold: true } },
>             { insert: '\n' },
>           ],
>         }}
>         cursorData={{
>           name: USER.name,
>           email: USER.email,
>           color: '#0f766e',
>           colorLight: '#0f766e33',
>         }}
>         modules={{
>           cursors: { transformOnTextChange: true },
>           history: { userOnly: true },
>           toolbar: [['bold', 'italic', 'underline'], [{ list: 'bullet' }]],
>         }}
>         onStatusChange={(status) => console.log('Status:', status)}
>         onError={console.error}
>       >
>         {(collaboration) => (
>           <nav>
>             <button onClick={() => collaboration.undo()}>Undo</button>
>             <button onClick={() => collaboration.redo()}>Redo</button>
>             <button onClick={() => collaboration.saveVersion('Checkpoint')}>
>               Save version
>             </button>
>             <span>{collaboration.synced ? 'Synced' : collaboration.status}</span>
>           </nav>
>         )}
>       </QuillCrdtEditor>
>     </main>
>   );
> }
>
> function DocumentScope() {
>   const user = useCurrentUser();
>   const { setDocuments } = useSetDocuments();
>   const [documentReady, setDocumentReady] = useState(false);
>
>   useEffect(() => {
>     if (!user) return;
>     setDocuments([{
>       id: DOCUMENT_ID,
>       metadata: { documentName: 'Shared Quill document' },
>     }]);
>     setDocumentReady(true);
>   }, [user, setDocuments]);
>
>   return user && documentReady
>     ? <CollaborativeEditor />
>     : <p>Preparing collaboration...</p>;
> }
>
> export default function App() {
>   const authProvider = useMemo(() => ({
>     user: USER,
>     retryConfig: { retryCount: 3, retryDelay: 1000 },
>     generateToken: async () => {
>       const response = await fetch('/api/velt/token', {
>         method: 'POST',
>         headers: { 'Content-Type': 'application/json' },
>         body: JSON.stringify(USER),
>       });
>       const { token } = await response.json();
>       return token;
>     },
>   }), []);
>
>   return (
>     <VeltProvider apiKey="YOUR_VELT_API_KEY" authProvider={authProvider}>
>       <DocumentScope />
>     </VeltProvider>
>   );
> }
> ```
>
> ```css Complete styles.css expandable lines theme={null}
> .quill-host {
>   width: 100%;
>   min-height: 360px;
> }
>
> .quill-host .ql-editor {
>   min-height: 320px;
> }
>
> .quill-host .ql-cursor,
> .quill-host .ql-cursor-selection {
>   visibility: visible;
>   opacity: 1;
> }
> ```

> [tab] Other Frameworks
>
> ```html Complete index.html expandable lines theme={null}
> <p>Status: <span id="status">connecting</span></p>
> <p>Synced: <span id="synced">No</span></p>
> <button id="undo">Undo</button>
> <button id="redo">Redo</button>
> <button id="save-version">Save version</button>
> <div id="quill-editor" class="quill-host"></div>
> ```
>
> ```ts Complete app.ts expandable lines theme={null}
> import Quill from 'quill';
> import QuillCursors from 'quill-cursors';
> import 'quill/dist/quill.snow.css';
> import { initVelt } from '@veltdev/client';
> import {
>   createCollaboration,
>   type CollaborationManager,
> } from '@veltdev/quill-crdt';
> import './styles.css';
>
> Quill.register('modules/cursors', QuillCursors);
>
> const DOCUMENT_ID = 'shared-quill-doc';
> const host = document.querySelector<HTMLElement>('#quill-editor')!;
>
> const client = await initVelt('YOUR_VELT_API_KEY');
> await client.identify({
>   userId: 'ada',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'my-org-id',
> });
> await client.setDocument(DOCUMENT_ID, {
>   documentName: 'Shared Quill document',
> });
>
> let resolveVeltReady!: () => void;
> const veltReady = new Promise<void>((resolve) => {
>   resolveVeltReady = resolve;
> });
> const initSubscription = client.getVeltInitState().subscribe((isReady) => {
>   if (isReady) resolveVeltReady();
> });
> await veltReady;
> initSubscription.unsubscribe();
>
> const editor = new Quill(host, {
>   theme: 'snow',
>   placeholder: 'Start writing...',
>   modules: {
>     cursors: { transformOnTextChange: true },
>     history: { userOnly: true },
>     toolbar: [
>       ['bold', 'italic', 'underline'],
>       [{ list: 'ordered' }, { list: 'bullet' }],
>       ['link', 'clean'],
>     ],
>   },
> });
>
> let manager: CollaborationManager | null = await createCollaboration({
>   editorId: DOCUMENT_ID,
>   veltClient: client,
>   editor,
>   initialContent: {
>     ops: [
>       { insert: 'Collaborative Quill document', attributes: { bold: true } },
>       { insert: '\n' },
>     ],
>   },
>   cursorData: {
>     name: 'Ada Lovelace',
>     email: 'ada@example.com',
>     color: '#0f766e',
>     colorLight: '#0f766e33',
>   },
>   onError: console.error,
> });
>
> document.querySelector('#status')!.textContent = manager.status;
> document.querySelector('#synced')!.textContent = manager.synced ? 'Yes' : 'No';
>
> const offStatus = manager.onStatusChange((status) => {
>   document.querySelector('#status')!.textContent = status;
> });
> const offSynced = manager.onSynced((synced) => {
>   document.querySelector('#synced')!.textContent = synced ? 'Yes' : 'No';
> });
> const offSelections = manager.onRemoteSelectionsChange((selections) => {
>   console.log('Remote selections:', selections);
> });
> const crdtSubscription = client.getCrdtElement()
>   .on('updateData')
>   .subscribe((event) => console.log('CRDT update:', event));
>
> document.querySelector('#undo')!.addEventListener('click', () => {
>   manager?.undo();
> });
> document.querySelector('#redo')!.addEventListener('click', () => {
>   manager?.redo();
> });
> document.querySelector('#save-version')!.addEventListener('click', () => {
>   void manager?.saveVersion('Manual checkpoint');
> });
>
> let cleanedUp = false;
> function cleanup() {
>   if (cleanedUp) return;
>   cleanedUp = true;
>   offStatus();
>   offSynced();
>   offSelections();
>   crdtSubscription.unsubscribe();
>   manager?.destroy();
>   manager = null;
>   host.replaceChildren();
> }
>
> window.addEventListener('pagehide', cleanup, { once: true });
> ```
>
> ```css Complete styles.css expandable lines theme={null}
> .quill-host {
>   width: 100%;
>   min-height: 360px;
> }
>
> .quill-host .ql-editor {
>   min-height: 320px;
> }
>
> .quill-host .ql-cursor,
> .quill-host .ql-cursor-selection {
>   visibility: visible;
>   opacity: 1;
> }
> ```

## 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

| Prop group | Important props |
| - | - |
| Collaboration | `editorId`, `documentId`, `initialContent`, `forceResetInitialContent`, `cursorData`, `debounceMs`, `undoCaptureTimeout`, `enabled` |
| Quill | `theme`, `placeholder`, `modules`, `formats`, `bounds`, `readOnly` |
| Host | `id`, `className`, `style`, `containerProps` |
| Lifecycle | `onStatusChange`, `onManagerReady`, `onEditorReady`, `onLoad`, `onError` |
| UI | `children` render function |

### React: useCollaboration()

| Return group | Fields and methods |
| - | - |
| Lifecycle | `editorRef()`, `manager`, `editor`, `primitives`, `isLoading`, `synced`, `isSynced`, `status`, `error`, `attachEditor()`, `detachEditor()`, `destroy()` |
| Text and Delta | `getValue()`, `setValue()`, `getContents()`, `setContents()`, `updateContents()`, `flushEditorToStore()`, `forceReset()` |
| Editing | `insertText()`, `deleteText()`, `insertEmbed()`, `formatText()`, `getFormat()`, `highlightRange()`, `clearHighlight()` |
| Awareness and history | `publishCurrentSelection()`, `setLocalSelection()`, `undo()`, `redo()` |
| Versions | `saveVersion()`, `getVersions()`, `getVersionById()`, `refreshSnapshots()`, `refreshVersions()`, `restoreVersion()`, `setStateFromVersion()` |

### CollaborationManager Methods

| Method group | Methods |
| - | - |
| Lifecycle | `initialize()`, `destroy()`, `attachEditor()`, `detachEditor()` |
| Text and Delta | `getValue()`, `getTextValue()`, `getContents()`, `getYTextDelta()`, `getSemanticHTML()`, `setValue()`, `setContents()`, `updateContents()`, `flushEditorToStore()`, `forceReset()` |
| Editing | `insertText()`, `deleteText()`, `insertEmbed()`, `formatText()`, `getFormat()`, `highlightRange()`, `clearHighlight()` |
| Awareness | `setLocalSelection()`, `publishCurrentSelection()`, `getRemoteSelections()`, `onRemoteSelectionsChange()` |
| History | `getUndoManager()`, `undo()`, `redo()`, `clearUndoHistory()` |
| Status | `initialized`, `synced`, `status`, `onStatusChange()`, `onSynced()`, `getStats()` |
| Versions | `saveVersion()`, `getVersions()`, `getVersionById()`, `restoreVersion()`, `setStateFromVersion()` |
| Advanced | `getCollaborationPrimitives()`, `getStore()`, `getDoc()`, `getText()`, `getYText()`, `getProvider()`, `getAwareness()` |

### Exported Types

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.