Commenting
In tldraw, a comment is a message pinned to a place on the canvas. Comments group into threads, one conversation per pin. Every comment records who wrote it, as an id your app resolves to a name.
The @tldraw/commenting package works at two levels. CanvasComments is a comments layer you render in front of the canvas, and it handles the whole flow. Everything it's built from is exported too, so you can replace any part of it.
Commenting is a licensed feature. It runs in development without a key. In production it needs a tldraw license that includes commenting.
Quick start
Three pieces: register the comment record types, register the comment tool, and render the layer.
import {
CanvasComments,
CommentAuthor,
commentToolOverrides,
commentTools,
} from '@tldraw/commenting'
import { useMemo } from 'react'
import { commentSchemaRecords, createTLSchema, createTLStore, TLComponents, Tldraw } from 'tldraw'
import '@tldraw/commenting/commenting.css'
import 'tldraw/tldraw.css'
const AUTHORS: Record<string, CommentAuthor> = { me: { name: 'You', color: '#EC5E41' } }
const resolveAuthor = (id: string) => AUTHORS[id]
const components: TLComponents = {
InFrontOfTheCanvas: () => <CanvasComments currentUserId="me" resolveAuthor={resolveAuthor} />,
}
export default function App() {
const store = useMemo(
() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
[]
)
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
licenseKey={YOUR_LICENSE_KEY}
store={store}
tools={commentTools}
overrides={[commentToolOverrides]}
components={components}
/>
</div>
)
}Pick the comment tool from Quick Actions, or press C, then click the canvas to start a thread.
The layer's two required inputs are both about identity: currentUserId is the id stamped on whatever the user posts, and resolveAuthor turns an author id into a name, a color, and an optional avatar image. Commenting reads no user directory of its own, so these are where you connect it to whatever your app already knows about its users. With the read-status and mention callbacks they make up the CommentingContext, which CanvasCommentsSidebar takes too — build it once and spread it into both.
Comments are records
Comment threads and comments are records in the editor's store, exactly like shapes. That's the fact most worth holding onto, because almost everything else follows from it.
They aren't in the default schema, so you opt in by registering commentSchemaRecords. Once you have, comments persist and sync however your document already does. Adding a persistenceKey or a sync backend carries them along with no extra work.
It also means you can read and write them yourself. Query the store for threads, seed a document with review notes, or build a panel that does something the built-in sidebar doesn't.
import { useCommentThreads } from '@tldraw/commenting'
import { useEditor } from 'tldraw'
function OpenThreadCount() {
const editor = useEditor()
const threads = useCommentThreads(editor)
return <div>{threads.filter((thread) => !thread.resolved).length} open threads</div>
}Anchors
What pins a thread to the canvas is its anchor. A thread can anchor to a point on the page, to a shape it then follows as that shape moves and resizes, to a rectangular region, or to the page as a whole.
Clicking empty canvas gives you a point anchor and clicking a shape gives you a shape anchor. Region anchors are off by default; turn them on and dragging the tool out covers an area.
Shape-anchored threads outlive their shape. Delete the shape and the thread converts to a point anchor where its pin last sat, so the conversation doesn't disappear along with the thing it was about.
What you get
The comments layer covers the whole flow out of the box:
| Feature | Description |
|---|---|
| Threads | Pins on the canvas, opening to replies, edit, resolve, and delete. |
| Mentions | @-mentions in composers, resolved against a roster you supply. |
| Reactions | Emoji reactions on comments, with a pluggable palette. |
| A sidebar | A filterable list of threads beside the canvas. |
| Clustering | Nearby pins fold into count badges as you zoom out. |
| Unread state | Pin badges and an unread filter, driven by your app's read data. |
Each of these has options, and each visible piece is a component slot you can replace. See Commenting for the full guide.
Configuring
Commenting options live on the tool. CommentTool.configure() returns a configured subclass to register, mirroring ShapeUtil.configure:
const tools = [CommentTool.configure({ enableRegions: true })]The option worth knowing about first is history. Comment writes default to 'ignore', which keeps them off the undo stack, and that default matters in a shared document: an undoable delete would resurrect a thread a collaborator already removed. See Comments and undo.
Permissions
canComment decides whether the viewer may participate, and the UI follows it: composers give way to a fallback slot and the action affordances hide. canModifyComment decides the writes that belong to someone in particular — editing a comment, deleting a comment, deleting a thread. Unset, each is its record's owner's to make; a callback widens that (a workspace admin who may remove anyone's comment) or narrows it.
They're UI-level controls and nothing more. Comment records carry a client-supplied author id, so rules about who may post, edit, or delete belong in your sync server's record authorization, where each incoming record is checked against the session's identity.
Syncing
Comments sync like the rest of your document, with one registration on each side. Pass records: commentSchemaRecords to your sync hook, and the same map to createTLSchema on the server.
On the server you can go a step further and serve comments through the room's object-store lane. Lane records are gated by their own per-session permission rather than by isReadonly, which is how a viewer who can't edit the document can still comment. See Syncing comments.
Related
- Commenting — The full guide: anchors, options, components, and sync
- Collaboration — Adding multiplayer to your project
- tldraw sync — Running a sync server
- Commenting example — The flow end to end