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.

Is this page helpful?
Prev
Multiplayer sync with custom presence
Next
Multiplayer sync with private content