---
title: "Collaboration on the whiteboard"
description: "Edit a whiteboard together in real time through Yjs, and show what other users select, drag and point at."
canonical_url: "https://tiptap-docs.lin52025i.qzz.io/editor/whiteboard/guides/collaboration"
---

# Collaboration on the whiteboard

Edit a whiteboard together in real time through Yjs, and show what other users select, drag and point at.

A board is one Tiptap document, so users edit it together through Yjs like any other document. The board adds what a whiteboard needs on top: `BoardCollaboration`, and helpers that show what other users select, drag and point at.

## Install

Collaboration lives in its own entry, `@tiptap-pro/extension-whiteboard/collaboration`. Only that entry needs Yjs, so a board without collaboration loads none. Install its peer dependencies, and `y-protocols`, which `@tiptap/y-tiptap` needs:

```bash
npm install @tiptap/extension-collaboration @tiptap/y-tiptap yjs y-protocols
```

To sync through Tiptap Collaboration, also install `@tiptap-pro/provider` from the private registry. See [Install Collaboration](https://tiptap-docs.lin52025i.qzz.io/collaboration/getting-started/install.md).

## Set up the editor

> **Use BoardCollaboration, and turn off the kit's undo:**
>
> Plain `Collaboration` drops a first change that leaves the document empty, like deleting the last
> shape, when the editor starts on content from Yjs. `BoardCollaboration` syncs that change too. Set
> `undoRedo: false` on the kit, so `undoBoard` and `redoBoard` use the undo of the collaboration.

```js
import { Editor } from '@tiptap/core'
import { WhiteboardKit } from '@tiptap-pro/extension-whiteboard'
import { BoardCollaboration } from '@tiptap-pro/extension-whiteboard/collaboration'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import * as Y from 'yjs'

const ydoc = new Y.Doc()

const provider = new TiptapCollabProvider({
  name: 'board-1', // The name of the board's document
  appId: 'your-app-id', // Your document server ID from the Cloud dashboard
  token: 'your-jwt',
  document: ydoc,
})

const editor = new Editor({
  element: viewport,
  extensions: [
    // Collaboration handles undo, so the kit leaves its own out
    WhiteboardKit.configure({ undoRedo: false }),
    BoardCollaboration.configure({ document: ydoc }),
  ],
})
```

In React, create the configured extensions once, outside the component or with `useMemo`. See [Set up a board in React](https://tiptap-docs.lin52025i.qzz.io/editor/whiteboard/guides/react.md).

`BoardCollaboration` takes the options of [Collaboration](https://tiptap-docs.lin52025i.qzz.io/editor/extensions/functionality/collaboration.md). The content of a collaborative board comes from Yjs. To give a new board its first shapes, see [Initialize content properly](https://tiptap-docs.lin52025i.qzz.io/collaboration/getting-started/install.md#initialize-content-properly).

## Show what others select and drag

Yjs syncs the document, but not what users select, or a shape while a user drags it. Share those through [awareness](https://tiptap-docs.lin52025i.qzz.io/collaboration/core-concepts/awareness.md):

- `getSelectedShapeIds` returns what this user selects. `setRemoteSelections` shows what the other users select.
- The board's editor emits `shapesMove` while a drag moves shapes, with `delta: null` once the drag ends. `createRemoteMoves` shows the drags of the other users, without changing the document. The other user's drop changes it.

```js
import { createRemoteMoves, getSelectedShapeIds } from '@tiptap-pro/extension-whiteboard'

provider.setAwarenessField('user', { name: 'Ada', color: '#be185d' })

// Shares what this user selects, when it changes
let sharedIds = []

editor.on('transaction', () => {
  const ids = getSelectedShapeIds(editor.state)

  if (ids.length !== sharedIds.length || ids.some((id, index) => id !== sharedIds[index])) {
    sharedIds = ids
    provider.setAwarenessField('selection', ids)
  }
})

// Shares the drags of this user, and `null` once a drag ends
editor.on('shapesMove', ({ ids, delta }) => {
  provider.setAwarenessField('drag', delta ? { ids, delta } : null)
})

// Shows what the other users select and drag
const moves = createRemoteMoves(editor)
let dragging = new Set()

provider.on('awarenessChange', ({ states }) => {
  const others = states.filter((state) => state.clientId !== ydoc.clientID)
  const stillDragging = new Set()

  editor.commands.setRemoteSelections(
    others.map((state) => ({ color: state.user?.color ?? '#6a00f5', ids: state.selection ?? [] })),
  )

  for (const { clientId, drag } of others) {
    if (drag) {
      moves.show({ owner: clientId, ids: drag.ids, delta: drag.delta })
      stillDragging.add(clientId)
    }
  }

  // Ends the drags that stopped, also of users who left
  for (const owner of dragging) {
    if (!stillDragging.has(owner)) {
      moves.end(owner)
    }
  }

  dragging = stillDragging
})
```

A shape that another user selects gets `data-remote-selected`, and that user's color in `--remote-selection`. When several users select a shape, the first one with a valid color shows. Only colors in the form `#RRGGBB`, like `#be185d`, pass, because the color goes into the style of the element. A selection with another color, like `red` or `#abc`, doesn't show:

```css
[data-shape][data-remote-selected] {
  outline: calc(2px / var(--zoom, 1)) solid var(--remote-selection);
}
```

## Show the cursors of others

`createRemoteCursors` shows where the other users point, with their names. It also moves the shapes that a user drags along with their cursor, in place of `createRemoteMoves`. Share the point under the pointer in board units, with the time and the drag, at most ten times a second. The pointer events come from `stage`, the element around the board, see [Stage and camera](https://tiptap-docs.lin52025i.qzz.io/editor/whiteboard/guides/stage-and-camera.md):

```js
import { clientToBoard, createRemoteCursors } from '@tiptap-pro/extension-whiteboard'

const stage = document.querySelector('.stage')
let point = null
let drag = null
let timer = null

// Shares where this user points, and what they drag, ten times a second at most
const share = () => {
  timer ??= setTimeout(() => {
    timer = null
    provider.setAwarenessField('cursor', point && { point, time: Date.now(), drag })
  }, 100)
}

stage.addEventListener('pointermove', (event) => {
  point = clientToBoard({ editor, point: { x: event.clientX, y: event.clientY } })
  share()
})

stage.addEventListener('pointerleave', () => {
  point = null
  share()
})

editor.on('shapesMove', ({ ids, delta }) => {
  drag = delta && { ids, delta }
  share()
})

// Shows the cursors of the other users
const cursors = createRemoteCursors(editor)

provider.on('awarenessChange', ({ states }) => {
  cursors.update(
    states
      .filter((state) => state.clientId !== ydoc.clientID && state.cursor)
      .map(({ clientId, user, cursor }) => ({
        owner: clientId,
        ...cursor,
        name: user?.name ?? '',
        color: user?.color ?? '',
      })),
  )
})
```

A cursor moves smoothly through the points of its user, a little late, at the pace they were shared. It fades out when its user stops pointing, or after a minute without moving.

The cursors are over the shapes and keep their size at every zoom, through `--zoom`. Each cursor has its user's color in `--remote-cursor`, with the same `#RRGGBB` rule as selections. Style its arrow and name:

```css
[data-remote-cursor-arrow] {
  fill: var(--remote-cursor);
  stroke: white;
  stroke-width: 2;
}

[data-remote-cursor-name] {
  padding: 2px 6px;
  color: white;
  font-size: 12px;
  background: var(--remote-cursor);
  border-radius: 4px;
}
```

## Doc frames

The board's Y.Doc holds the frames, but not their documents. Your `createEditor` decides where each document lives and how it syncs. See [Doc frames](https://tiptap-docs.lin52025i.qzz.io/editor/whiteboard/guides/doc-frames.md).

`start` runs only in the editor whose change brings a frame onto the board. Frames that come through Yjs keep the document that their own editor started. So do frames that an undo with Yjs brings back.

## Heights of text

Free text and doc frames grow as tall as their content. Each editor measures the changes that it makes, with its own fonts. Changes by other users keep the height that their editor measured.

## Update all editors together

Use the same version of the Whiteboard extension in all editors of a board, and update them at the same time. An older editor can't read content that only newer versions have and can delete that content for everyone. An editor on 0.1.0, for example, can't read arrows with labels.

## Next steps

- Show suggestions on the board with [Tracked Changes](https://tiptap-docs.lin52025i.qzz.io/editor/whiteboard/guides/tracked-changes.md)
- Learn more about [Tiptap Collaboration](https://tiptap-docs.lin52025i.qzz.io/collaboration/getting-started/overview.md)
