Collaboration (Beta)

Customize comment actions

Add, remove, and reorder the actions on a comment with commentActions and CommentActionBar.

The actions on a comment are an array. commentActions builds the package's default list, CommentActionBar renders it, and both are exported, so adding, removing, and reordering entries are array operations rather than props. This guide covers each of those, and where an action's flow lives.

Prerequisites:

  • A comment layout that renders its own CommentActionBar. The default Comment builds this list internally. See Build a custom comment.

Build the default list

Every snippet on this page imports from one list:

import {SDKCommentsProvider} from '@sanity/collaboration/sdk'
import {
  AddReactionButton,
  CommentActionBar,
  commentActions,
  CommentsProvider,
  DeleteCommentDialog,
  useCommentsContext,
} from '@sanity/collaboration/ui'
import {WarningOutlineIcon} from '@sanity/icons/WarningOutline'
import {Flex} from '@sanity/ui'
import {useState} from 'react'
const {resolveCapabilities} = useCommentsContext()
const capabilities = resolveCapabilities(comment)

const actions = commentActions({
  comment,
  capabilities,
  onStatusChange,
  onEdit,
  onDelete,
  onCopyLink,
  onRetry,
})

comment is the comment your card renders, from its props. onStatusChange, onEdit, onDelete, onCopyLink, and onRetry are your card's own handlers. Pass the ones the card supports.

comment is typed as the fields the list branches on, so a whole CommentData fits and so does an object carrying only those. capabilities comes from useCommentsContext().resolveCapabilities(comment), which applies the provider's policy for you. Unlike commentActions, resolveCapabilities needs the whole CommentData.

It returns resolve or reopen, retry, edit, copy link, and delete, in that order. An entry appears only when its callback was passed, and is disabled when the matching capability resolved to false: a missing callback renders nothing, and a forbidden one renders grayed out. Two entries have a second condition: resolve or reopen appears only on a thread parent, a comment with no parentCommentId, and retry appears only on a comment whose state.type is 'createError'.

The order is part of the contract, because it decides which entries CommentActionBar promotes to buttons. How many it promotes is maxVisible, covered in CommentActionBar.

Add an action

An entry is a CommentAction: a name, an action, and optionally an icon, a disabled flag, and a critical tone. The full shape is in Helpers.

const [flagged, setFlagged] = useState(false)

const actions = [
  ...commentActions({comment, capabilities, onEdit, onDelete}),
  {
    name: 'Mark as spam',
    icon: WarningOutlineIcon,
    action: () => setFlagged(true),
  },
]

Then render <CommentActionBar actions={actions} />, and show flagged however your card marks a flagged comment.

An entry with no icon always goes to the overflow menu, since an icon button with no icon is nothing to click.

Remove or reorder an action

// No copy link on this surface.
const actions = commentActions({
  comment,
  capabilities,
  onEdit,
  onDelete,
}).filter((action) => action.name !== 'Copy link')

Leaving onCopyLink out is the better version of that, because an action with no callback is never built in the first place. Filtering is for the case where the same callbacks are wired but one surface should not offer the action.

To reorder, build the list in parts and concatenate them. Each part keeps the default order within it, and the result goes to CommentActionBar as usual:

// Edit first, so CommentActionBar promotes it to a button.
const actions = [
  ...commentActions({comment, capabilities, onEdit}),
  ...commentActions({comment, capabilities, onStatusChange, onDelete}),
]

Own the flow an action starts

An action reports that the user asked for something. CommentActionBar does not own the flow an entry starts, which is what lets you replace the flow without replacing the action.

Edit flips your own state and renders CommentInput:

const [editing, setEditing] = useState(false)

commentActions({comment, capabilities, onEdit: () => setEditing(true)})

Delete opens DeleteCommentDialog, and the dialog's confirm is what reaches the SDK:

const [deletePending, setDeletePending] = useState(false)

commentActions({comment, capabilities, onDelete: () => setDeletePending(true)})

<DeleteCommentDialog
  open={deletePending}
  onClose={() => setDeletePending(false)}
  onConfirm={() => remove(comment.id)}
/>

The default Comment does both of these internally, which is why its onEdit takes a message rather than firing on the menu click.

Let capabilities decide what is disabled

Per capability, the resolution order is the component's capabilities prop, then the provider's policy, then the built-in defaults. In a layout of your own, the first of those is the optional second argument to resolveCapabilities(comment, overrides). canResolve covers reopening too, and canCreate covers retrying a failed create.

A policy goes on the provider. In an App SDK app that is SDKCommentsProvider. This one disables deletion on resolved comments:

<SDKCommentsProvider
  capabilities={(comment) =>
    comment.status === 'resolved' ? {canDelete: false} : {}
  }
>
  {children}
</SDKCommentsProvider>

CommentsProvider takes the same capabilities prop. A role check goes there too:

<CommentsProvider
  currentUser={me}
  getUser={getUser}
  // `isAdmin` is your app's own role check.
  capabilities={() => ({canDelete: isAdmin})}
>
  {children}
</CommentsProvider>

The defaults are author ownership for edit and delete, thread status for reply, and true for resolve, reactions, and canCreate, which covers retry. The resolution order in full is in Providers.

Reactions are not actions

AddReactionButton opens its own popover rather than running a handler, so it composes beside CommentActionBar, or inside it as children, rather than as an entry in the array:

<ActionRow align="center" gap={1}>
  <AddReactionButton
    reactions={comment.reactions}
    onReactionToggle={toggleReaction}
  />
  <CommentActionBar actions={actions} />
</ActionRow>

ActionRow is your card's own styled Flex, the one its hover reveal targets. See Declare the hover reveal.

Give it reactions and it marks what the current user has already picked.

Next steps

Was this page helpful?

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