Collaboration (Beta)

Anchor inline comments

Write an inline comment anchored to Portable Text with the App SDK, and re-anchor it when the text moves.

An inline comment is anchored to a run of text inside a Portable Text field rather than to the field as a whole. This guide writes one with the SDK directly, anchors it into text that has not been saved yet, and re-anchors it when the text moves.

If you render SDKPortableTextComments, it does all of this for you. See Comment on Portable Text. This page is for an editor of your own, or for writing inline comments with no editor at all.

Prerequisites:

  • Familiarity with Write comments, including the toMessage helper the examples on this page use to wrap plain text as a message.
  • A Portable Text field, and the _key values of its blocks.
  • @sanity/sdk-react 3.11.0 or later, the first release with the @sanity/sdk-react/collaboration entry point.

The concepts, including why the stored anchor survives edits, are in Inline comments.

Pass an anchor when creating

An anchor names an offset into each end of the run, by block _key and character offset into that block's plain text:

import {useCommentActions} from '@sanity/sdk-react/collaboration'

const {createComment} = useCommentActions()

await createComment({
  documentId,
  documentType: 'article',
  fieldPath: 'body',
  message: toMessage('Tighten this up'),
  anchor: {
    type: 'portable-text',
    start: {_key: 'b1', offset: 0},
    end: {_key: 'b1', offset: 12},
  },
})

fieldPath is required with an anchor, since the anchor is a run of text inside that field. Without it, createComment rejects with the error A comment with an anchor needs a field path to anchor it in.

Read what the API resolved

The anchor is what you write, not what you read back. The API resolves it, and the comment comes back with:

  • selection, the blocks the comment covers. Each carries the block's entire text, with the sentinel characters U+F000 and U+F001 marking where the run starts and ends. Storing marked text rather than offsets is what lets a highlight survive edits elsewhere in the same block.
  • contentSnapshot, a copy of the part of each block the selection covers, as it was when the comment was written.

Both appear once the server has answered: the optimistic comment carries neither.

Turning a selection back into a position in a live editor needs the editor's current value, so that lives in @portabletext/plugin-sdk-value rather than in the SDK.

Anchor into unsaved text

The API resolves an anchor against the document it holds. When the offsets count into text nobody has saved yet, that lands the comment on the wrong words, or on nothing.

Set fieldValue on the anchor to the blocks the offsets belong to, and the API resolves it against those instead:

import {useCommentActions} from '@sanity/sdk-react/collaboration'

const {createComment} = useCommentActions()

await createComment({
  documentId,
  documentType: 'article',
  fieldPath: 'body',
  message: toMessage('Tighten this up'),
  anchor: {
    type: 'portable-text',
    start: {_key: 'b1', offset: 0},
    end: {_key: 'b1', offset: 12},
    fieldValue: editorValue,
  },
})

editorValue is the editor's current blocks. The blocks from the start _key through the end _key are enough; the whole field is fine too.

Re-anchor when the text moves

When the anchored text moves, write its new position with updateCommentAnchor. It exists so that a mechanical move does not mark the comment edited, as updateComment would, and it takes a fieldValue for the same reason createComment does:

import {useCommentActions} from '@sanity/sdk-react/collaboration'

const {updateCommentAnchor} = useCommentActions()

const anchor = {
  type: 'portable-text',
  start: {_key: 'b1', offset: 4},
  end: {_key: 'b1', offset: 16},
} as const

await updateCommentAnchor({
  commentId,
  anchor: {...anchor, fieldValue: editorValue},
})

anchor has three meanings:

anchor

Effect

A new anchor

Re-anchors. The new selection shows once the server has resolved it.

null

Drops the anchor, leaving a field-level comment on the same field. Shows straight away.

Left out

Does nothing, and sends no request.

Write once the typing stops rather than on every keystroke: each call is a request. SDKPortableTextComments waits for a pause, and writes each moved comment once.

Re-anchor only from an editor on the comment's own sourceDocumentId. Outside a release, the comment read hooks return draft and published comments together by default. Re-anchoring a published comment from the draft's text rewrites it against words it was never about.

Related

Was this page helpful?

Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change site theme