Collaboration (Beta)

Introduction to the comment components

React components for comment threads on Sanity documents: replies, mentions, reactions, resolve, and deep links, rendering data from the App SDK.

@sanity/collaboration is a set of React components for commenting on Sanity content. The components are built on Sanity UI, and none of them fetches anything: comment data arrives as props, from the App SDK or from anywhere else.

This is a paid feature

This feature is available in the Growth plan.

Comments are stored per organization and reached through the collaboration API.

With comments, you can:

  • Put a comments panel next to your own editing UI, with threads, replies, and resolve.
  • Mention teammates and have the mention resolve to a name and an avatar from your own user directory or from the SDK's.
  • Share a link that opens on one comment, scrolls to it, and expands it out from behind collapsed replies.
  • Comment on a run of text inside a Portable Text field, with a highlight that stays on those words as the text around them is edited.
  • Build a comment card of your own from the same primitives the default one is assembled from, keeping the skeletons, accessible names, and action list.

Start here

Requirements

@sanity/collaboration declares these peer dependencies:

Requirement

Version

react and react-dom

^19.2

@sanity/ui

^4.0.6

styled-components

^6.1

@sanity/icons

^5.2.1

@sanity/sdk

^3.11.0

@sanity/sdk-react

^3.11.0

@portabletext/editor

^8.2.4

@portabletext/editor is required rather than optional, even for a panel: the composer is a Portable Text editor. A bundler that honors sideEffects: false can still drop the inline comment code from an app that never renders it.

@sanity/sdk and @sanity/sdk-react are optional peers, needed only for @sanity/collaboration/sdk: SDKCommentsProvider, SDKCommentsPanel, SDKPortableTextComments, and useSDKThreadCallbacks. The components and hooks come from @sanity/collaboration/ui, and the data types and helpers come from the package root. Neither entry point imports the SDK, so a surface with no SDK, such as a test, a story, or an app with its own data layer, uses just those two.

Two more things the components assume:

  • An organization: Comments are scoped to one, but you don't name it. The SDK uses the organization that owns the project. If the Dashboard or organizationId on the config names a different one, that's an error.
  • Sanity UI's stylesheet: Sanity UI v4 ships its static styles as a plain stylesheet, so import '@sanity/ui/styles.css' goes wherever your app loads global CSS.

Core concepts

Seven ideas cover the whole surface. Each one is a page, and each page links to the guides that put it to work. The reasoning behind the API shape, and what it buys you, is in Design principles of the comment components.

Every prop, hook, and type is in the Storybook reference. The SDK hooks the components read from are covered in Comments in the App SDK.

Threads and comments

A thread hangs off one field of one document and holds the comment that started it plus its replies. There is no separate thread record to create: writing the first comment on a field creates the thread, and the SDK returns the two together. A thread is the unit a card renders, and fieldPath is the grouping key everywhere.

A comment carries its author ID, its message as Portable Text, and the status the UI renders from. See Threads and comments.

Users and identity

Comments store user IDs, not names. Names and avatars are looked up through a synchronous getUser function on the provider, which makes a user directory the one thing no comments surface can do without.

SDKCommentsProvider fills that in from the SDK, and CommentsProvider takes your own answers instead. See Users and identity.

Components

Five components cover a panel: CommentList, CommentThread, Comment, CommentInput, and CommentBadge. Every primitive the default Comment is assembled from is also exported, so a layout of your own is composed from finished pieces rather than rebuilt.

Actions are data: an action renders only when you passed its callback, and is disabled when the matching capability resolves to false. See Comment components.

Data flow

The components own no data. Reads come from the SDK's useCommentThreads or useCommentsQuery, writes from useCommentActions, and the wiring between them is a page of your own code.

Reads suspend on first load and writes apply optimistically, which decides what your callbacks look like: one line each, returning nothing. See Comment data flow.

Navigation

CommentsProvider makes everything below it selectable and linkable. Left alone it keeps the address in state; hand it your router and a pasted link opens the panel on the comment it names. Either way the components own everything downstream of that value, including matching, scrolling, focus, and expanding collapsed replies.

What passes between you is a value and a callback rather than a router integration, so this works with React Router, Next.js, and with no router at all. See Comment navigation and deep links.

Inline comments

A comment on a run of text inside a Portable Text field is drawn as a highlight under those words. The anchor is stored as marked-up text rather than offsets, so it survives edits around it.

The components come from @sanity/collaboration/ui with the rest of the UI, and @portabletext/editor is a peer dependency alongside them. See Inline comments.

Levels of abstraction

The stack is two ladders you pick from independently: default components, primitives, or your own UI for what renders; and SDK hooks, SDK actions, the Sanity client, or raw HTTP for where the data comes from.

Nothing forces you to take a whole level. A panel built from the default components can read through the HTTP API, and a UI of your own can still take the SDK's optimistic writes. See Levels of abstraction in the comment stack.

Limitations

  • No permission-aware read of who may be mentioned. useUsers returns the users on a resource, not the subset allowed to see a given document. Filter that list yourself, or mark the difference with granted so the picker shows an ungrantable user rather than hiding them. See Enable mentions.
  • Nothing reports who may comment, resolve, or delete. No read answers it, so the provider's capabilities policy is where your own answer goes. The built-in defaults tie edit and delete to the comment's author and reply to an open thread, and allow resolve, react, and retry for everyone.
  • No count-only read. A badge's number comes from reading the document's threads once and grouping them yourself. See Show comment counts.
  • A failed edit rolls back silently unless you pass onError. The local state a comment can carry covers a failed create only, so a rejected updateComment reverts the message with nothing on screen to say why. useSDKThreadCallbacks reports it to an onError you pass, which is where your own error goes. See Show a failed write.
  • Read-only is capabilities, not a switch. A panel's threadProps capabilities reach only its cards. An inline editor beside it can still start threads, unless the provider's policy turns them off too.
  • Retrying a create is not guaranteed idempotent. Retry re-issues createComment with the same comment ID. If the first request reached the server, the API can reject the retry as a conflict.
  • One string cannot be translated. The provider's labels reaches every other string the components render, but the delete dialog's close button keeps Sanity UI's own Close dialog. See Translate the UI text.

Was this page helpful?

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