Commenting
Add comment threads to the canvas with the commenting toolkit.
import {
CanvasComments,
CommentAuthor,
CommentTool,
commentToolOverrides,
filterMentionMembers,
MentionMember,
} from '@tldraw/commenting'
import { getLicenseKey } from '@tldraw/dotcom-shared'
import { useMemo } from 'react'
import { commentSchemaRecords, createTLSchema, createTLStore, TLComponents, Tldraw } from 'tldraw'
import '@tldraw/commenting/commenting.css'
import 'tldraw/tldraw.css'
// A demo avatar image (inline SVG) so one author shows an image instead of a colored initial.
const ADA_AVATAR =
'data:image/svg+xml,' +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect width="28" height="28" fill="#0E9F6E"/><circle cx="14" cy="11" r="5" fill="#fff"/><ellipse cx="14" cy="24" rx="9" ry="7" fill="#fff"/></svg>`
)
// The people who can be @-mentioned. A real app would pull this from its own roster; the composer
// filters this list as you type after `@`. Ids match the author directory below.
const MEMBERS: MentionMember[] = [
{ id: 'me', name: 'You', color: '#EC5E41', you: true },
{ id: 'ada', name: 'Ada Lovelace', color: '#0E9F6E', image: ADA_AVATAR },
{ id: 'grace', name: 'Grace Hopper', color: '#4465E9' },
{ id: 'alan', name: 'Alan Turing', color: '#9C1FBE' },
]
// A tiny local user directory so the flow shows names, colors, and images instead of ids. A real
// app would resolve these from its own identity system.
const AUTHORS: Record<string, CommentAuthor> = Object.fromEntries(MEMBERS.map((m) => [m.id, m]))
const resolveAuthor = (id: string): CommentAuthor => AUTHORS[id] ?? { name: id }
// Region comments are off by default. With `enableRegions`, dragging the comment tool out draws a
// rectangle and anchors the comment to that area instead of a point or shape.
const COMMENT_TOOLS = [CommentTool.configure({ enableRegions: true })]
export default function CommentingExample() {
// Comments are stored as `comment-thread` and `comment` records in the editor's own store.
// Registering `commentSchemaRecords` on the schema is all it takes to persist and sync them
// alongside shapes — no separate backend, so the whole flow runs in-memory here.
const store = useMemo(
() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
[]
)
// `CanvasComments` reads those records reactively and draws the pins, threads, and composer.
// Mounting it in front of the canvas is the entire UI layer.
const components = useMemo<TLComponents>(
() => ({
InFrontOfTheCanvas: () => (
<CanvasComments
currentUserId="me"
resolveAuthor={resolveAuthor}
getMentionSuggestions={(query) => filterMentionMembers(MEMBERS, query)}
/>
),
}),
[]
)
return (
<div className="tldraw__editor">
<Tldraw
// Commenting is a licensed feature. Every feature is enabled in local development, but a
// deployed app needs a license key that includes commenting — swap in your own key here.
licenseKey={getLicenseKey()}
store={store}
tools={COMMENT_TOOLS}
overrides={[commentToolOverrides]}
components={components}
/>
</div>
)
}
Comment threads are records in the editor's store, so they persist and sync exactly like shapes. Register commentSchemaRecords on the schema, add CommentTool and commentToolOverrides to the editor, then render CanvasComments in the InFrontOfTheCanvas slot. That is the whole integration.
Press c or pick the comment tool, then click anywhere to start a thread, click a shape to attach one to it, or drag to comment on a region (turned on here with CommentTool.configure({ enableRegions: true })). Type @ in a composer to mention someone; the suggestions come from getMentionSuggestions.
Commenting is a licensed feature. Everything works in local development, but a deployed app needs a license key that includes commenting.