Get started with a custom collaborative canvas using Liveblocks and Next.js

This guide shows you how to build a custom collaborative canvas in a Next.js /app directory application. You will synchronize draggable boxes with Storage, add multiplayer undo/redo, then show live cursors and selections with presence.

Using a canvas library?

Follow the Tldraw quickstart if you want a complete canvas renderer and editing toolkit instead of building the interactions yourself.

Quickstart

  1. Install and initialize Liveblocks

    Install @liveblocks/client and @liveblocks/react. Every Liveblocks package should use the same version.

    Terminal

    Initialize liveblocks.config.ts, which will hold your application's collaborative types.

    Terminal
  2. Define the canvas Storage types

    Each box has a position and fill color. Store the boxes in a LiveMap, keyed by stable IDs, and make each box a LiveObject so its properties can be updated independently.

    liveblocks.config.ts

    Use Storage for canvas data that must persist. Cursor and selection state will be added to presence in the final step.

  3. Create a Liveblocks room

    Liveblocks rooms are collaborative spaces where users work on the same data. Set up a LiveblocksProvider, join a room with RoomProvider, and provide the initial boxes for a new room.

    app/Room.tsx

    This guide uses a fixed room ID so everyone opening the page joins the same canvas. In a real application, derive a stable room ID from the document or project being opened.

    Wrap your canvas page in the room.

    app/page.tsx
  4. Render and select boxes

    Read an immutable snapshot of the shared boxes with useStorage. Keep the current selection in local React state for now. The effect clears the selection if another user deletes that box.

    app/Canvas.tsx

    Open the page in two browser tabs. Both tabs render the same two boxes from the room's Storage document.

  5. Add and delete boxes

    Use useMutation to modify the mutable Storage structures. Add these imports and mutations inside Canvas.

    app/Canvas.tsx

    Add a toolbar before the canvas. A newly created box becomes the local selection, and deleting a box clears that selection.

    app/Canvas.tsx

    Storage mutations are synchronized to every connected user and added to the current user's multiplayer history.

  6. Drag boxes

    Capture the pointer when a drag begins, remember the pointer's offset inside the box, and update Storage on every pointer movement. The mutation safely does nothing if another user deletes the box mid-drag.

    app/Canvas.tsx

    Connect the handlers to the canvas and each box. Pointer capture keeps sending events while the pointer moves outside the selected box.

    app/Canvas.tsx

    Each pointer move updates the box's x and y properties, so other connected users see the complete drag rather than only its final position.

  7. Add multiplayer undo and redo

    Add history controls with useUndo, useRedo, useCanUndo, and useCanRedo.

    app/Canvas.tsx

    A drag generates many Storage mutations. Use useHistory to pause history after pointer capture and resume it when the gesture ends, making the complete drag one undo step. Route every termination event through the same idempotent function, and use it during cleanup so history cannot remain paused if the component unmounts mid-drag.

    app/Canvas.tsx

    Replace the three end-event handlers on the canvas with finishDrag.

    app/Canvas.tsx

    Each user's history contains their own changes. Undoing your drag does not reverse a change made by another collaborator.

  8. Add live cursors and selections with presence

    Finish the canvas by moving the current selection from local React state into presence and adding cursor coordinates. Presence is temporary, per-connection state, so it is not saved in the canvas document.

    First, add the presence type to liveblocks.config.ts.

    liveblocks.config.ts

    Set the initial presence values when entering the room.

    app/Room.tsx

    In Canvas, remove the selectedShapeId useState call and the now unused useState import. Read your own selection with useSelf, update it with useUpdateMyPresence, and read collaborators with useOthers.

    app/Canvas.tsx

    Replace every local selection update with selectShape. This keeps add, delete, drag, and canvas-background selections visible to other users.

    app/Canvas.tsx

    Publish canvas-relative cursor coordinates while preserving the existing drag handler. Clear the cursor when it leaves the canvas.

    app/Canvas.tsx

    Finally, show when another user has selected a box and render their cursor. Keep the current user's selection blue and use purple for other users.

    app/Canvas.tsx

    Open the canvas in two tabs. Boxes now move continuously in both tabs, each drag is one undo step, and each tab shows the other user's cursor and selection.

What to read next

Congratulations! You’ve built the foundation of a custom multiplayer canvas for your Next.js application.


Examples using Next.js