Creating a Margin Action with Decorator in Sanity Block Editor

6 replies
Last updated: Feb 28, 2022
Hi all! Does anyone know how you'd go about creating a Margin Action in the block editor that applies a decorator to the current block? I'm trying to create a button in the margin that will allow the user to add a "comment" on a block-by-block basis:
AI Update

Creating a Margin Action in Sanity's block editor to apply decorators is a bit tricky because the Portable Text Editor doesn't have a built-in API specifically for "margin actions" that apply decorators to entire blocks. However, I can guide you through the closest approaches to achieve what you're looking for.

Understanding the Challenge

Decorators in Sanity's Portable Text are designed to work on inline text selections, not entire blocks. They're simple string-based formatting marks like bold, italic, or custom styles applied to spans of text. This means you can't directly apply a decorator to a whole block from a margin button in the traditional sense.

Option 1: Use Custom Block Styles (Best for Block-Level Comments)

Instead of decorators, use custom block styles which apply to entire blocks. This is more semantically correct for your use case:

import {defineType} from 'sanity'
import {CommentIcon} from '@sanity/icons'

export default defineType({
  name: 'blockContent',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'Heading 1', value: 'h1'},
        // Your custom comment style
        {
          title: 'Commented',
          value: 'commented',
          icon: CommentIcon
        }
      ]
    }
  ]
})

Users can then select a block and change its style from the style dropdown in the toolbar. While this isn't a margin button, it achieves block-level marking.

Option 2: Use Annotations with Metadata

For actual commenting functionality where you want to store comment text, use annotations instead of decorators. Annotations can carry additional data:

marks: {
  annotations: [
    {
      name: 'comment',
      type: 'object',
      title: 'Comment',
      icon: CommentIcon,
      fields: [
        {
          name: 'text',
          type: 'text',
          title: 'Comment Text'
        },
        {
          name: 'author',
          type: 'string',
          title: 'Author'
        }
      ]
    }
  ]
}

This allows users to highlight text and add comments with metadata, which is more powerful than simple decorators.

Option 3: Custom Block Type with Comment Field

For true block-level commenting, create a wrapper block type:

{
  name: 'commentedBlock',
  type: 'object',
  title: 'Block with Comment',
  fields: [
    {
      name: 'content',
      type: 'block',
      title: 'Content'
    },
    {
      name: 'comment',
      type: 'text',
      title: 'Comment'
    }
  ]
}

Then include this in your block content array:

of: [
  {type: 'block'},
  {type: 'commentedBlock'}
]

Why Margin Actions for Decorators Don't Exist

The Portable Text Editor's architecture separates concerns: decorators operate on text selections (controlled by the user highlighting text), while block-level operations use styles or custom block types. There isn't a built-in "margin action" API that bridges these two concepts because it would be semantically confusing—decorators are meant for inline formatting, not block-level metadata.

If you absolutely need a custom UI button in the margin area, you'd need to create a custom Studio plugin with significant custom React components that hook into the Portable Text Editor's internal APIs, which is quite advanced and not officially documented for this specific use case.

Recommendation

For a commenting feature on blocks, I'd strongly recommend Option 2 (annotations) if you want inline comments, or Option 1 (custom block styles) if you just need to visually mark entire blocks as "commented" without storing additional data. Both work with the editor's existing UI patterns and don't require custom margin buttons.

Show original thread
6 replies
The example in the docs (https://www.sanity.io/docs/customization#7c452491a5f2 ) shows how to insert a new text block after the current block, rather than customising the current block (in this case applying a decorator). Can anyone help me?
This is my margin action:

// BlockMarginCommentAction.js
import React from "react";
import { Button } from "@sanity/ui";
import { CommentIcon } from "../../styles/Icons";

function BlockMarginCommentAction(props) {
    const handleClick = (event) => {
        const { insert, set, block, value } = props;
        insert([
            {
                _type: "block",
                children: [
                    {
                        _type: "span",
                        text: "New text block!",
                    },
                ],
            },
        ]);
        // Instead of inserting a new block, add a decorator to the current block?
    };
    return (
        <Button
            icon={CommentIcon}
            padding={2}
            onClick={handleClick}
            title="Add a comment to this block."
            mode="bleed"
        />
    );
}

export default BlockMarginCommentAction;
And this is the decorator I'm trying to apply:

import { CommentIcon } from "../../styles/Icons";

export default {
    name: "blockComment",
    title: "Comment",
    type: "object",
    icon: CommentIcon,
    options: {
        editModal: "dialog",
    },
    fields: [
        {
            name: "comment",
            title: "Comment",
            type: "text",
        },
        {
            name: "reference",
            title: "Reference to page:",
            type: "reference",
            to: [{ type: "page" }],
        },
    ],
};

This is a tricky one, but you'd have to use the
set
function to set the current block to a block that has the correct
markDefs
. For example:
const handleClick = () => {
    const id = uuid()

    set({
      ...block,
      markDefs: [
        { 
          _type: 'blockComment',  
          _key: id,
          comment:'this is a comment',
        }
      ],
      children: [
        {
          ...block.children[0],
          marks: [ id ]
        }
      ]
    })
  }
However, allowing an editor to control what
markDefs.comment
contains through a component will be quite tricky. You can't use something like
useState
to open a modal or popup because hooks are not allowed within the Portable Text Editor.
Hey Racheal, thanks so much for looking at this! Your example is exactly what I was trying to accomplish. Hopefully hooks might be allowed in the future, as it would be nice to trigger a popup after the margin action is pressed šŸ¤ž
Hey Racheal, thanks so much for looking at this! Your example is exactly what I was trying to accomplish. Hopefully hooks might be allowed in the future, as it would be nice to trigger a popup after the margin action is pressed šŸ¤ž
It would be super powerful to be able to have this functionality!
It would be super powerful to be able to have this functionality!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?