Comment anchors

The ways a comment can attach to the canvas.

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 thread carries an anchor that says where on the page it lives: a point, a shape it tracks as that shape moves and resizes, or a region covering an area. A fourth kind, page, has no pin and surfaces in a list instead.

This example seeds one of each pinned kind, with a shape anchor shown both precise and imprecise. 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