Collaboration (Beta)

Use comments in Sanity Studio

Use the comment components in a studio plugin, custom document view, or custom input, with the studio's theme and current user.

A comments panel works in a Sanity Studio plugin, a custom document view, or a custom input, and most of the setup an app needs is already done for you there. This guide covers what a studio changes: the theme, where identity comes from, and deep links.

A studio shares comments with your App SDK apps only when its config turns on Comments API v2 with beta: {comments: {v2: true}}. The option is still internal and can change. With it on, the studio reads and writes sanity.comment documents through the same collaboration API as this package, so a thread started in your app shows up in the studio, and the reverse. Without it, the studio uses the comments that predate Comments API v2, which are stored per dataset as comment documents, and neither side sees the other's threads.

import {defineConfig} from 'sanity'

export default defineConfig({
  // ...
  beta: {comments: {v2: true}},
})

Prerequisites:

  • @sanity/collaboration installed. See Get started, Step 1. The studio already has the Sanity UI peers.
  • A studio plugin, custom document view, or custom input to mount the comments panel in.

What a studio already provides

A studio has Sanity UI's stylesheet and a Sanity UI theme above your component, which is everything the components need. They take the studio's palette and color scheme, so a comments panel in a dark studio is dark. You import no stylesheet and set up no theme.

See Theme the comment components.

Supply identity from the studio

Both SDKCommentsProvider and CommentsProvider work in a studio. A studio mounts its own App SDK resource so that SDK hooks work anywhere in it, which means SDKCommentsProvider resolves identity in a studio the way it does in an app.

Prefer CommentsProvider all the same. The studio already has the current user and a user store, and passing them keeps the panel on the identity the studio around it is showing, rather than resolving the same people a second way and without a fallback while it does.

useCurrentUser and useUserStore from sanity supply both. Map each studio user to the CommentUser shape, and look users up as the comments ask for them:

import type {CommentUser} from '@sanity/collaboration'
import {CommentsProvider} from '@sanity/collaboration/ui'
import {type ReactNode, useCallback, useRef, useState} from 'react'
import {useCurrentUser, useUserStore} from 'sanity'

export function StudioCommentsProvider({children}: {children: ReactNode}) {
  const currentUser = useCurrentUser()
  const userStore = useUserStore()
  const [users, setUsers] = useState(new Map<string, CommentUser | null>())
  const requested = useRef(new Set<string>())

  const getUser = useCallback(
    (id: string) => {
      if (users.has(id)) return users.get(id)
      if (!requested.current.has(id)) {
        requested.current.add(id)
        userStore.getUser(id).then((user) =>
          setUsers((prev) =>
            new Map(prev).set(
              id,
              user
                ? {id: user.id, displayName: user.displayName, imageUrl: user.imageUrl}
                : null,
            ),
          ),
        )
      }
      return undefined
    },
    [users, userStore],
  )

  if (!currentUser) return null

  return (
    <CommentsProvider
      currentUser={{
        id: currentUser.id,
        displayName: currentUser.name,
        imageUrl: currentUser.profileImage,
      }}
      getUser={getUser}
    >
      {children}
    </CommentsProvider>
  )
}

getUser answers undefined while a user is being fetched, which renders a skeleton, and null once the store says there is no such user. Each answer that arrives replaces the map, so getUser gets a new identity and the skeletons re-render into names. Render your panel inside StudioCommentsProvider.

A view that takes this route imports from @sanity/collaboration/ui and never from @sanity/collaboration/sdk, so nothing in the panel depends on the SDK resource.

Either way, the panel inside the provider is identical. Return null rather than undefined for an ID your store has settled on as absent, or that comment shows a skeleton forever. See Users and identity.

<system> is the author ID on comments Sanity writes itself. It resolves to "Sanity" with the Sanity logo whatever your lookup returns, which is the same user the studio shows.

Link with the Sanity Studio router

A shared link opens the panel on a comment the same way in a studio as in an app, with the studio router holding the address. Link to a comment covers the props and helpers this uses, and what to do when a link names a comment the panel is hiding.

The Sanity Studio router keeps query parameters on RouterState._searchParams as [key, value] pairs, which URLSearchParams both accepts and spreads back to.

import {parseCommentAddress, withCommentAddress} from '@sanity/collaboration'
import {SDKCommentsProvider} from '@sanity/collaboration/sdk'
import {useMemo} from 'react'
import {useRouter, useRouterState} from 'sanity/router'

function Comments() {
  const router = useRouter()
  const searchParams = useRouterState((state) => state._searchParams)

  const params = useMemo(
    () => new URLSearchParams(searchParams ?? []),
    [searchParams],
  )

  return (
    <SDKCommentsProvider
      activeComment={parseCommentAddress(params)}
      onActiveCommentChange={(next) =>
        router.navigate(
          {
            ...router.state,
            _searchParams: [...withCommentAddress(params, next)],
          },
          {replace: true},
        )
      }
    >
      {/* … */}
    </SDKCommentsProvider>
  )
}

Next steps

Was this page helpful?

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