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

<Tabs>
  <Tab title="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>

  <Tab title="Other Frameworks">
    ```bash theme={null}
    npm install @veltdev/quill-crdt @veltdev/crdt @veltdev/client quill quill-cursors y-quill yjs y-protocols
    ```
  </Tab>
</Tabs>

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';
```

<Tabs>
  <Tab title="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>

  <Tab title="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);
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="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>

  <Tab title="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'],
        ],
      },
    };
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="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>

  <Tab title="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();
    ```
  </Tab>
</Tabs>

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

### Step 5: Initialize Collaboration

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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)`.
  </Tab>
</Tabs>

#### 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.</Warning>

### Step 8: Monitor Status and Remote Selections

<Tabs>
  <Tab title="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>

  <Tab title="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);
    });
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="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>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const crdtSubscription = client.getCrdtElement()
      .on('updateData')
      .subscribe((event) => console.log('CRDT update:', event));
    ```
  </Tab>
</Tabs>

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

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

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collaboration = useCollaboration({
      editorId: 'shared-quill-doc',
      initializeWithoutEditor: true,
    });

    collaboration.attachEditor(editor);
    collaboration.detachEditor();
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
    });

    manager.attachEditor(editor);
    manager.detachEditor();
    ```
  </Tab>
</Tabs>

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)

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <VeltProvider
      apiKey="YOUR_VELT_API_KEY"
      authProvider={authProvider}
      encryptionProvider={encryptionProvider}
    >
      <DocumentScope />
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    client.setEncryptionProvider(encryptionProvider);
    ```
  </Tab>
</Tabs>

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

### Step 15: Clean Up

<Tabs>
  <Tab title="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>

  <Tab title="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();
    ```
  </Tab>
</Tabs>

### 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](/docs/realtime-collaboration/crdt/setup/core#veltcrdtstoremap) to inspect shared Delta state and provider status.
</Tip>

## Complete Example

<Tabs>
  <Tab title="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>

  <Tab title="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;
    }
    ```
  </Tab>
</Tabs>

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