---
title: "Whiteboard stage and camera"
description: "Connect a whiteboard to the stage around it, and pan, zoom and fit the board with the camera helpers."
canonical_url: "https://tiptap.dev/docs/editor/whiteboard/guides/stage-and-camera"
---

# Whiteboard stage and camera

Connect a whiteboard to the stage around it, and pan, zoom and fit the board with the camera helpers.

A board lives in three elements. The **stage** is the element around the board. It takes pointer and wheel events, and clips everything outside it. Inside the stage, the **viewport** holds the editor. The **camera** moves and scales the viewport.

## The camera

The camera is part of your app's state:

```ts
type Camera = {
  x: number
  y: number
  zoom: number
}
```

`x` and `y` are in screen pixels, and a `zoom` of 1 is 100%. The camera helpers keep the zoom between `minZoom`, which is 10%, and `maxZoom`, which is 400%.

Show the board with the camera: move the viewport with `toCssTransform`, and set `--zoom` to the zoom. Handles read `--zoom` to keep their size at every zoom.

```js
import { toCssTransform } from '@tiptap-pro/extension-whiteboard'

toCssTransform({ x: 100, y: 50, zoom: 2 }) // 'translate(100px, 50px) scale(2)'
```

The viewport needs `transform-origin: 0 0`. See [Add the stage CSS](https://tiptap.dev/docs/editor/whiteboard/guides/react.md#add-the-stage-css) for the CSS of the stage and the viewport.

## Attach the stage

`attachStage` connects the board of an editor to its stage. It returns a function that detaches everything again:

```js
import { attachStage } from '@tiptap-pro/extension-whiteboard'

const detach = attachStage({ stage, editor, updateCamera: setCamera })
```

`updateCamera` changes the camera through an update function, like a React state setter. In React, pass the setter of a `useState`. Without React, apply the update yourself, see [Basic setup](https://tiptap.dev/docs/editor/whiteboard/getting-started/overview.md#basic-setup).

Once attached, users can work with the board in the stage. Its keys work while the focus is in the stage, so the rest of the page, and other boards on it, keep theirs:

- **Pan**: drag with space held, with the middle mouse button, or with the `hand` tool. Turning the wheel or scrolling on a trackpad pans too. On a touch screen, drag the empty board with one finger while the `select` tool is active, or move two fingers. A pan wins over drawing and selecting. A pan or a click with the `hand` tool gives the board the keyboard, like a click with `select`, so its keys work right after a toolbar button chose the tool.
- **Zoom**: Control + wheel, or Cmd + wheel on macOS, or a pinch on a trackpad, zooms at the pointer. On a touch screen, a pinch with two fingers zooms at the middle between them. The second finger stops what the first one started, and undoes it, like moving a shape.
- **Zoom shortcuts**: Control + = or + zooms in, Control + - zooms out, and Control + 0 goes back to 100%, around the middle of the stage, instead of zooming the page. On macOS, use Cmd instead of Control. Keys in a field are left to the field.
- **Fit**: Shift + 1 fits all shapes into view while no text is edited.
- **Draw**: with a shape tool, a drag on the board adds a shape. A click adds a note, text, doc frame or geometric shape at a fixed size for its tool, centered on the pointer. The pen draws lines, and the eraser erases the shapes it touches. A doc frame stays when the erase starts on it, so users can erase the pen lines on a document. Drawing also works over other shapes.
- **Select**: with the `select` tool, a drag of the mouse or a pen on the empty board selects the shapes that its box touches. Hold Shift to add them to the selection. A click or a tap on the empty board clears the selection.

Escape cancels a drag while it runs, except a pan.

### Keep room for your controls

Shift + 1 keeps 64 screen pixels free at each edge of the stage. When panels
float over the stage, keep more room with `fitInsets`:

```js
import { attachStage } from '@tiptap-pro/extension-whiteboard'

attachStage({
  stage,
  editor,
  updateCamera: setCamera,
  // Keeps shapes out from under the toolbar when Shift + 1 fits them into view
  fitInsets: () => ({ top: 80, right: 24, bottom: 24, left: 24 }),
})
```

### Show the tool and the pan in CSS

The stage shows the active tool as `data-tool`, and a pan as `data-pan`: `ready` while space is held, and `active` while the board pans. Use them for cursors:

```css
.stage:where([data-tool]:not([data-tool='select'], [data-tool='hand'])),
.stage:where([data-tool]:not([data-tool='select'], [data-tool='hand'])) * {
  cursor: crosshair;
}

.stage[data-tool='hand'],
.stage[data-tool='hand'] *,
.stage[data-pan='ready'],
.stage[data-pan='ready'] * {
  cursor: grab;
}

.stage[data-pan='active'],
.stage[data-pan='active'] * {
  cursor: grabbing;
}
```

While a drag runs, the stage holds a preview element: `data-selection-box`, `data-shape-preview`, `data-arrow-preview` or `data-pen-preview`. See [Styling](https://tiptap.dev/docs/editor/whiteboard/api-reference/styling.md#stage).

## Zoom buttons

`zoomAtStageCenter` changes the zoom around the middle of the stage, so the board stays put there. `stepZoom` returns the next zoom step in a direction, from 10% to 400%:

```js
import { stepZoom, zoomAtStageCenter } from '@tiptap-pro/extension-whiteboard'

// Zoom in one step
zoomAtStageCenter({
  stage,
  updateCamera: setCamera,
  nextZoom: (zoom) => stepZoom({ zoom, direction: 1 }),
})

// Back to 100%
zoomAtStageCenter({ stage, updateCamera: setCamera, nextZoom: () => 1 })
```

A zoom between two steps goes to the next step in that direction. For example, `stepZoom({ zoom: 0.8, direction: -1 })` returns `0.75`.

## Fit shapes into view

`zoomToFitShapes` moves and zooms the camera so every shape is in the stage. It never zooms in beyond 100%, or out beyond `minZoom`, and does nothing on an empty board:

```js
import { zoomToFitShapes } from '@tiptap-pro/extension-whiteboard'

zoomToFitShapes({ stage, editor, updateCamera: setCamera })
```

`scrollShapesIntoView` moves the camera as little as it can, so the shapes with the given ids are in the stage. It zooms out only when they don't fit:

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

scrollShapesIntoView({
  stage,
  editor,
  ids: getSelectedShapeIds(editor.state),
  updateCamera: setCamera,
})
```

Both keep 64 screen pixels free at each edge. Pass `insets` to change that room, with one number for every edge or one for each.

To show a part of the board, like when the board opens, `fitCameraToRect` returns the camera that shows a rectangle in the middle of the stage:

```js
import { fitCameraToRect } from '@tiptap-pro/extension-whiteboard'

const camera = fitCameraToRect({
  rect: { x: 0, y: 0, width: 1200, height: 800 },
  viewport: { width: stage.clientWidth, height: stage.clientHeight },
})
```

It zooms out until the rectangle fits, but not beyond `minZoom`, and by default never zooms in beyond 100%. Set `largestZoom` to allow more.

## Convert coordinates

Shapes use board units. `clientToBoard` converts a point in window coordinates, like the `clientX` and `clientY` of a pointer event, to board units. It returns `null` while the board isn't on the page, like before `useEditor`'s editor is mounted, or once the editor is destroyed:

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

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

  if (point) {
    console.log(`The pointer is at ${point.x}, ${point.y} on the board`)
  }
})
```

`visibleBoardRect` returns the part of the board that the stage shows. Use it to add a shape where users look:

```js
import { stickyNoteSize, visibleBoardRect } from '@tiptap-pro/extension-whiteboard'

const seen = visibleBoardRect({
  camera,
  viewport: { width: stage.clientWidth, height: stage.clientHeight },
})
const { width, height } = stickyNoteSize

// A new note in the middle of the stage
editor.commands.insertStickyNote({
  x: seen.x + (seen.width - width) / 2,
  y: seen.y + (seen.height - height) / 2,
})
```

## Next steps

- Add and change [shapes, and choose tools](https://tiptap.dev/docs/editor/whiteboard/guides/shapes-and-tools.md)
- See all [utilities](https://tiptap.dev/docs/editor/whiteboard/api-reference/utilities.md), with the stage and camera helpers
