Comment anchors
Seed threads with each kind of anchor: a point, a shape (precise and imprecise), and a region.
import {
CanvasComments,
CommentAuthor,
commentToolOverrides,
commentTools,
putCommentRecords,
} from '@tldraw/commenting'
import { getLicenseKey } from '@tldraw/dotcom-shared'
import { useMemo } from 'react'
import {
commentSchemaRecords,
createComment,
createCommentThread,
createShapeId,
createTLSchema,
createTLStore,
Editor,
TLCommentAnchor,
TLComponents,
Tldraw,
toRichText,
} from 'tldraw'
import '@tldraw/commenting/commenting.css'
import 'tldraw/tldraw.css'
// A demo avatar image (inline SVG) so Ada's comments show 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>`
)
const AUTHORS: Record<string, CommentAuthor> = {
ada: { name: 'Ada Lovelace', color: '#0E9F6E', image: ADA_AVATAR },
me: { name: 'You', color: '#EC5E41' },
}
const resolveAuthor = (id: string): CommentAuthor => AUTHORS[id] ?? { name: id }
// A thread plus its opening comment, anchored however the caller specifies. Every `TLCommentThread`
// carries an `anchor` — a discriminated union — and `CanvasComments` renders each kind in the right
// place: shape pins track the shape, point/region pins sit at fixed page coordinates.
function seedThread(editor: Editor, anchor: TLCommentAnchor, text: string) {
const pageId = editor.getCurrentPageId()
const thread = createCommentThread({ pageId, anchor, createdBy: 'ada' })
const comment = createComment({
threadId: thread.id,
pageId,
authorId: 'ada',
body: toRichText(text),
})
putCommentRecords(editor, [thread, comment])
}
export default function CommentAnchorsExample() {
const store = useMemo(
() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
[]
)
const handleMount = (editor: Editor) => {
// A rectangle to anchor shape comments against.
const boxId = createShapeId()
editor.run(
() => {
editor.createShapes([
{ id: boxId, type: 'geo', x: 120, y: 100, props: { geo: 'rectangle', w: 200, h: 140 } },
])
// shape (imprecise): normalized x/y ignored for the pin, which sits at the shape's
// top-right badge spot. The anchor still tracks the shape as it moves and resizes.
seedThread(
editor,
{ type: 'shape', shapeId: boxId, x: 1, y: 0, isPrecise: false },
'Anchored to this shape (imprecise — sits at the corner).'
)
// shape (precise): the pin sits exactly at the stored normalized spot inside the shape.
seedThread(
editor,
{ type: 'shape', shapeId: boxId, x: 0.5, y: 0.6, isPrecise: true },
'Anchored to a precise spot inside the shape.'
)
// point: a bare page coordinate, unattached to any shape.
seedThread(editor, { type: 'point', x: 200, y: 340 }, 'Anchored to a point on the page.')
// region: a rectangular area; the pin sits on its corner and the box is drawn.
seedThread(
editor,
{ type: 'region', x: 380, y: 300, w: 200, h: 130 },
'Anchored to a region of the page.'
)
},
{ history: 'ignore' }
)
editor.zoomToBounds({ x: 60, y: 60, w: 600, h: 420 }, { immediate: true })
}
const components = useMemo<TLComponents>(
() => ({
InFrontOfTheCanvas: () => <CanvasComments currentUserId="me" resolveAuthor={resolveAuthor} />,
}),
[]
)
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}
onMount={handleMount}
tools={commentTools}
overrides={[commentToolOverrides]}
components={components}
/>
</div>
)
}
Every TLCommentThread carries an anchor that says where on the page it lives: a point at fixed page coordinates, a shape that tracks the shape as it moves and resizes, or a region covering a rectangular area. A fourth kind, page, has no pin and surfaces only in a list.
Shape anchors store a normalized x/y inside the shape's bounds. When isPrecise is true the pin sits exactly there; when false the pin sits at the impreciseShapeAnchor option (the top-right corner by default) and the anchor addresses the shape as a whole.
The example creates each thread with createCommentThread and createComment, then writes them with putCommentRecords. Drag the shape to watch its pins follow it.