> 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).

# Show a comment preview

Show one comment in a popover from a badge or a highlighted range, and make a comments panel read-only.

A comment in a popover, shown from a badge or from a highlighted range in an editor. This guide builds one out of `Comment`, and covers how to make a panel read-only.

Prerequisites:

- A `CommentsProvider` around the surface, and a thread to preview. See [Get started](https://www.sanity.io/docs/collaboration/get-started).

## Render the preview

There is no `CommentPreview` component, and there does not need to be one. A `Comment` with no callbacks renders no actions, `showReactions={false}` drops the reaction row, and `maxLines` clamps the message:

**CommentPreview.tsx**

```tsx
import {type CommentThreadData} from '@sanity/collaboration'
import {Comment} from '@sanity/collaboration/ui'
import {Stack, Text} from '@sanity/ui'
import {Popover} from '@sanity/ui/popover'

export function CommentPreview({
  thread,
  open,
  referenceElement,
}: {
  thread: CommentThreadData
  open: boolean
  referenceElement: HTMLElement | null
}) {
  return (
    <Popover
      open={open}
      referenceElement={referenceElement}
      content={
        <Stack gap={3} padding={2}>
          <Comment
            comment={thread.parentComment}
            variant="reply"
            showReactions={false}
            maxLines={3}
          />

          {thread.commentsCount > 1 && (
            <Text muted>
              {thread.commentsCount === 2
                ? '1 reply'
                : `${thread.commentsCount - 1} replies`}
            </Text>
          )}
        </Stack>
      }
    />
  )
}
```

`Popover` comes from `@sanity/ui/popover`. The root `@sanity/ui` export of it is a placeholder in Sanity UI v4, and fails to typecheck with `JSX element type 'Popover' does not have any construct or call signatures.` The preview goes in `content`, which renders inside the popover. The `children` prop is the element the popover anchors to, not its body.

The reply count is a line of your own, because a preview's copy belongs to the surface showing it. Translate it along with the rest of your UI.

The trigger is yours too. This badge keeps its own element in state with `ref={setElement}`, so the popover has something to anchor to, and opens the preview while the pointer is over it:

**FieldBadge.tsx**

```tsx
import type {CommentThreadData} from '@sanity/collaboration'
import {CommentBadge} from '@sanity/collaboration/ui'
import {useState} from 'react'

import {CommentPreview} from './CommentPreview'

export function FieldBadge({thread}: {thread: CommentThreadData}) {
  const [element, setElement] = useState<HTMLButtonElement | null>(null)
  const [hovered, setHovered] = useState(false)

  return (
    <>
      <CommentBadge
        count={thread.commentsCount}
        ref={setElement}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
      />
      <CommentPreview
        thread={thread}
        open={hovered}
        referenceElement={element}
      />
    </>
  )
}
```

`variant="reply"` leaves out the quoted snapshot, which the parent already shows. In a popover that is usually what you want: the preview opens from the words the comment is about, so quoting them again only takes up room. Pass `showContentSnapshot={false}` instead to say so explicitly.

## Why passing no callbacks is enough

An action renders only when its callback is present, so passing none leaves `Comment` with nothing to render but the comment itself. There is no separate read-only rendering to keep in step with the real one. See [Comment components](https://www.sanity.io/docs/collaboration/components).

## Clamp the message

`maxLines` clamps the message to that many lines, which keeps a popover a predictable size whatever the comment says:

```tsx
<Comment comment={thread.parentComment} maxLines={3} />
```

## Make the panel read-only

For a panel where nothing should be editable, such as an audit view or an export preview, turn every capability off and leave out `onThreadCreate`:

**ReadOnlyPanel.tsx**

```tsx
import type {CommentThreadData} from '@sanity/collaboration'
import {CommentsPanel, READ_ONLY_CAPABILITIES} from '@sanity/collaboration/ui'

export function ReadOnlyPanel({threads}: {threads: CommentThreadData[]}) {
  return (
    <CommentsPanel
      threads={threads}
      threadProps={() => ({capabilities: () => READ_ONLY_CAPABILITIES})}
    />
  )
}
```

For the whole surface, pass `capabilities={() => READ_ONLY_CAPABILITIES}` to the provider instead. The composers and the reaction picker go; resolve, reopen, the menu entries and the reaction pills stay, disabled. Selecting a comment and copying its link still work.

`SDKCommentsPanel` always wires `onThreadCreate`, so for a read-only view with no add-thread buttons, use `CommentsPanel` with `useCommentThreads` from `@sanity/sdk-react/collaboration`.

For a standalone `Comment`, pass `capabilities={READ_ONLY_CAPABILITIES}`, the object itself rather than a function returning it. Or leave out its write callbacks, as `CommentPreview` does.

## Next steps

- Where the badge that opens this comes from: [Show comment counts](https://www.sanity.io/docs/collaboration/show-comment-counts).
- The rest of the `Comment` props: [Comment](https://collaboration-platform.sanity.dev/?path=/docs/default-comment--docs).
- How capabilities resolve: [Providers](https://collaboration-platform.sanity.dev/?path=/docs/default-providers--docs#capabilities).
- The read-only panel, rendered: [CommentsPanel](https://collaboration-platform.sanity.dev/?path=/docs/default-commentspanel--docs).
- A hover preview and a read-only panel, as an example you can run: [document-list](https://github.com/sanity-io/collaboration-platform-examples/tree/main/examples/document-list).

