> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Customize inline comments

Common ways to change what SDKPortableTextComments does once inline comments are wired into your editor.

Established ways to change what `SDKPortableTextComments` does, once the basic wiring from [Comment on Portable Text](https://www.sanity.io/docs/collaboration/comment-on-portable-text) is in place. Each pattern is self-contained: take the one you need.

Prerequisites:

- An editor with `SDKPortableTextComments` mounted. See [Comment on Portable Text](https://www.sanity.io/docs/collaboration/comment-on-portable-text).

## Select a new thread as soon as it exists

When to use this: the panel beside the editor should scroll to and select a thread the moment someone creates it, which is what most authors expect.

`onCommentCreated` hands you the comment as the SDK returns it:

```tsx
import {useActiveComment} from '@sanity/collaboration/ui'
import {SDKPortableTextComments} from '@sanity/collaboration/sdk'

const {setActiveComment} = useActiveComment()

<SDKPortableTextComments
  {...handle}
  path="body"
  threads={threads}
  onCommentCreated={(comment) => setActiveComment(comment.id)}
/>
```

`useActiveComment` comes from `@sanity/collaboration/ui` and throws outside a `CommentsProvider`, so the editor and the panel go under one of those. See [Comment navigation and deep links](https://www.sanity.io/docs/collaboration/navigation).

## Move or relabel the composer

When to use this: the default "Comment" affordance is in the wrong place for your layout, or its copy is wrong for your product.

The composer is part of the plugin, and writing it out as the plugin's children replaces the default one:

```tsx
import {InlineCommentInput} from '@sanity/collaboration/ui'
import {SDKPortableTextComments} from '@sanity/collaboration/sdk'

<SDKPortableTextComments {...handle} path="body" threads={threads}>
  <InlineCommentInput label="Add note" placement="top" />
</SDKPortableTextComments>
```

`placement` is where the popovers open relative to the selection, and `inputProps` forwards the rest to the underlying `CommentInput`.

> [!WARNING]
> `InlineCommentInput` is internal
> It is exported and meant to be used, and tagged `@internal` to say its props may change in a minor release. The plugin renders it for you, so write it out when the default placement or copy stops fitting rather than by preference.

For a composer that is not a popover at all, build your own from `useInlineCommentAuthoring` and pass that as the children instead. The hook gives you the commentable selection, the pending one, the element to position against, and `begin`, `cancel`, and `submit`. Its full shape is in the [Portable Text Storybook reference](https://collaboration-platform.sanity.dev/?path=/docs/portable-text--docs#useinlinecommentauthoring).

## Draw your own highlight

When to use this: the default yellow mark does not fit your design, or the highlight needs to carry something of its own.

`renderHighlight` is called once per comment with the comment and its state, and returns the function the editor calls to wrap the words:

```tsx
<SDKPortableTextComments
  {...handle}
  path="body"
  threads={threads}
  renderHighlight={(comment, {selected, onSelect}) =>
    (props) => (
      // The editor owns the keyboard inside the text, so the mark has no
      // key handler of its own.
      // oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
      <mark
        data-comment-id={comment.id}
        data-active={selected || undefined}
        onClick={onSelect}
      >
        {props.children}
      </mark>
    )}
/>
```

Two rules come with it:

- **The returned function is not a component.** The editor calls it directly rather than mounting it, so it must not use hooks. Anything stateful goes in a component of your own that it renders.
- **onSelect is absent when navigation is not wired.** Check for it rather than assuming it, so a custom mark does not look clickable for nothing.

Threads on overlapping words nest one mark inside the other, and a comment that contains another is drawn outside it, so the innermost mark is the narrowest comment. The default mark steps its fill by how many threads cover a run, up to three deep. A replacement takes that on. The run under a comment being written is drawn by `renderPendingHighlight`, which keeps the default mark until you pass your own.

## Comment on a nested Portable Text field

When to use this: a block inside your Portable Text holds Portable Text of its own, such as a callout with a `content` array.

That inner array is edited by an editor of its own, and it gets its own plugin with the array's full path. The editor reads and writes the nested path with the same `useDocument`, `useEditDocument`, and `ValueSync` as in [Step 3](https://www.sanity.io/docs/collaboration/comment-on-portable-text), whose imports and `schemaDefinition` it reuses:

```tsx
const path = 'body[_key=="callout-1"].content'

function CalloutField({handle}: {handle: DocumentHandle}) {
  const {threads} = useCommentThreads({...handle, status: 'open'})
  const {data} = useDocument<PortableTextBlock[] | undefined, string>({
    ...handle,
    path,
  })
  const editField = useEditDocument<PortableTextBlock[] | undefined>({
    ...handle,
    path,
  })

  return (
    <EditorProvider initialConfig={{schemaDefinition, initialValue: data}}>
      <ValueSync value={data} onChange={editField} />
      <PortableTextEditable />
      <SDKPortableTextComments {...handle} path={path} threads={threads} />
    </EditorProvider>
  )
}
```

Comments there carry the longer `fieldPath`, and each plugin draws the comments whose path falls under its own. The plugin on the nested array leaves comments on `body` itself alone. Both take the same `threads`. A selection made inside the nested editor produces the nested path when a comment is created from it.

## Run without the SDK's writes

When to use this: the host already has its own write path, such as a test, a story, or an app with its own data layer.

`PortableTextComments` is the same component with the flat comments as props and every write reported to a callback:

```tsx
import {PortableTextComments} from '@sanity/collaboration/ui'

// `api` is your own write path. Here it takes the same shapes as the SDK's
// `createComment` and `updateCommentAnchor`.
<PortableTextComments
  path="body"
  comments={comments}
  sourceDocumentId="drafts.post-123"
  onCreate={({fieldPath, range, fieldValue, message}) =>
    api.createComment({
      ...handle,
      fieldPath,
      message,
      anchor: {type: 'portable-text', ...range, fieldValue},
    })
  }
  onRangeChange={(commentId, change) =>
    void api.updateCommentAnchor({
      commentId,
      anchor:
        change.range === null
          ? null
          : {
              type: 'portable-text',
              ...change.range,
              fieldValue: change.fieldValue,
            },
    })
  }
/>
```

Writing to the SDK yourself, the `range` and `fieldValue` that `onCreate` hands you become the comment's `anchor`: `{type: 'portable-text', ...range, fieldValue}`. In `onRangeChange`, a `range` of `null` means the words are gone: pass `anchor: null` to `updateCommentAnchor`, which leaves a field-level comment.

`sourceDocumentId` is what stops this editor from re-anchoring a comment written against another variant of the document. It is optional here, and `SDKPortableTextComments` derives it from the handle.

> [!WARNING]
> Leaving `onRangeChange` out turns re-anchoring off. The highlights still follow edits on screen, and only the write stops. Do that only if something else keeps the stored anchors current, since an anchor left behind drifts for the next reader.

## Next steps

- Why an anchor survives edits, and what re-anchoring writes: [Inline comments](https://www.sanity.io/docs/collaboration/inline-comments).
- Every prop and callback on both components: [Portable Text Storybook reference](https://collaboration-platform.sanity.dev/?path=/docs/portable-text--docs).

