Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive

Customizing the Portable Text Editor

Customization guidelines and examples to tailor the Portable Text editor to your needs.

Sanity Studio Portable Text editor is customizable so that it can fit different editorial needs. You can configure and tailor several different editors throughout the studio.
For more information about configuring the editor, see Configuring the Portable Text Editor.

In general, customization works by passing React components to the schema definitions of content types that use Portable Text. Alternatively, it's also possible to pass strings.

Toolbar icons and span rendering

When you configure custom markers, that is, decorators (simple values) and annotations (rich data structures), they are displayed as icons in the toolbar. The default icon is a question mark. You can customize it to display a different icon.

If you add custom decorators and annotations, you may want to control their visual presentation in the editor. By default, decorators are invisible, whereas annotations have a gray background and a dotted underline.

Decorators

Some often-used decorators, such as strong, emphasis, and code, feature rendering out of the box.

For example, let’s say you created a decorator to highlight text using the following configuration:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
{ title: 'Highlight', value: 'highlight' }
] } } ] }

Now, add a custom toolbar icon by passing in an anonymous function that returns H as a string to .icon:

// RichTextEditor.jsx
export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
          { 
title: 'Highlight',
value: 'highlight',
icon: () => 'H'
} ] } } ] }

The string is rendered in the decorator button in the toolbar:

Toolbar with custom decorator button

You can also pass a JSX component directly in the schema, or via an import.
The following example adds simple inline styling to a span holding the character H.

// RichTextEditor.js
import React from 'react'

const HighlightIcon = () => (
<span style={{fontWeight: 'bold'}}>H</span>
)
export default { name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', marks: { decorators: [ { title: 'Strong', value: 'strong' }, { title: 'Emphasis', value: 'em' }, { title: 'Code', value: 'code' }, {
title: 'Highlight',
value: 'highlight',
icon: HighlightIcon
} ] } } ] }

The next step is to render the actual highlighted text in the editor. We do this by passing the props into a React component and wrapping them in a span with some styling.

// RichTextEditor.js
import React from 'react'

const HighlightIcon = () => (
  <span style={{ fontWeight: 'bold' }}>H</span>
)
const HighlightDecorator = props => (
<span style={{ backgroundColor: 'yellow' }}>{props.children}</span>
)
export default { name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', marks: { decorators: [ { title: 'Strong', value: 'strong' }, { title: 'Emphasis', value: 'em' }, { title: 'Code', value: 'code' }, { title: 'Highlight', value: 'highlight', icon: HighlightIcon,
component: HighlightDecorator
} ] } } ] }

The rendered presentation in the editor is a yellow background for highlighted text:

Editor with custom render and icon for the highlight decorator

Protip

When you create your custom decorators, you can keep all, some, or none of the built-in decorators.

These are the built-in decorators:

{ "title": "Strong", "value": "strong" },
{ "title": "Emphasis", "value": "em" },
{ "title": "Code", "value": "code" },
{ "title": "Underline", "value": "underline" },
{ "title": "Strike", "value": "strike-through" }

Make sure you include those you intend to keep.

Annotations

Customizing annotations works much in the same way as decorations: you pass an icon and a renderer in the schema definition.

A common use case is to have an annotation for an internal reference, in addition to a link with an external URL.
You can customize the editor to display a custom icon for the internal link, and a renderer that helps recognize external links when they are inline in the text.

The following example imports an icon from the @sanity/icons-package. In the example, you configure a user icon to represent internal references to a person type.

// RichTextEditor.js
import { UserIcon } from '@sanity/icons'
export default { name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', marks: { annotations: [ { name: 'link', type: 'object', title: 'link', fields: [ { name: 'url', type: 'url' } ] }, { name: 'internalLink', type: 'object', title: 'Internal link',
icon: UserIcon,
fields: [ { name: 'reference', type: 'reference', to: [ { type: 'person' } // other types you may want to link to ] } ] } ] } } ] }

Now the user icon replaces the default question mark icon in the toolbar:

The editor with a custom user icon for the internal link annotation

Custom components

The next step is to create a custom renderer for external links. The following example appends an "arrow out of a box" icon to mark these links. To do this, you pass a small React component.

In the /schemas/components directory, create a file and name it ExternalLinkRenderer.tsx.

// ExternalLinkRenderer.js
import React from 'react'
import { LaunchIcon } from '@sanity/icons'

const ExternalLinkRenderer = props => (
  <span>
    {props.renderDefault(props)}
    <a contentEditable={false} href={props.value.href}>
      <LaunchIcon />
    </a>
  </span>
)

export default ExternalLinkRenderer

Then, import the following component, and pass it to components.annotation in the schema type:

// RichTextEditor.js
import { UserIcon } from '@sanity/icons'
import ExternalLinkRenderer from './components/ExternalLinkRenderer'
export default { name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', marks: { annotations: [ { name: 'link', type: 'object', title: 'link', fields: [ { name: 'url', type: 'url' } ], components: {
annotation: ExternalLinkRenderer
} }, { name: 'internalLink', type: 'object', title: 'Internal link', icon: UserIcon fields: [ { name: 'reference', type: 'reference', to: [ { type: 'person' } // other types you may want to link to ] } ] } ] } } ] }

As a result, external links now look like this:

The editor with custom renderer for external links.

Block styles

The Portable Text editor ships with a set of styles that translate well to their corresponding HTML ones. However, your front end may not be targeting HTML. While it has always been possible to add and configure block styles, you can now also configure how these styles render in the editor using markup and the styling method of your choice. This means you can tune your editor to be aligned with your organization’s design system.

To illustrate this point, the following example produces a custom title style using Garamond as the font face with a slightly increased font size. First, define a custom style called title:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'Title', value: 'title'},
        {title: 'H1', value: 'h1'},
        {title: 'H2', value: 'h2'},
        {title: 'H3', value: 'h3'},
        {title: 'Quote', value: 'blockquote'},
      ]
    }
  ]
}

Without any customization, the block looks exactly like the normal one. To apply a custom style to it, create a renderer in React.
It works in the same way as renderers for marks: pass a React component to component. The props of the block contain the element to style and the appropriate styling.
In the following example, the React component is added to the configuration file.

// RichTextEditor.js
import React from 'react'

const TitleStyle = props => (
<span style={{fontFamily: 'Garamond', fontSize: '2em'}}>{props.children} </span>
)
export default { name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', styles: [ {title: 'Normal', value: 'normal'}, {title: 'H1', value: 'h1'}, {title: 'H2', value: 'h2'}, {title: 'H3', value: 'h3'}, {title: 'Quote', value: 'blockquote'}, { title: 'Title', value: 'title',
component: TitleStyle
}, ] } ] }

The component prop applies the custom style to the title block, as it's rendered in the editor:

The editor with a custom title block style

Validation of annotations

Like other content types, annotations support content validation. Warnings are displayed in the margin and in the document. A pointer activates the annotation modal for the editor. Validations help editors structure the content correctly. It's generally a good idea to involve editors in creating validations and testing the warning messages so that they are helpful for them.

Let's say that you are using the same content for multiple websites. In this case, it's important that internal linking use an annotation with a reference input. This helps prevent accidental deletion of linked content and resolve internal links in the front-end project.
You can create a simple validation that takes care of this:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      validation: Rule => Rule.regex(/.*damnation.*/gi, { name: 'profanity' }),
      marks: {
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'link',
            fields: [
              {
                name: 'url',
                type: 'url',
                validation: Rule =>
                  Rule.regex(
                    /https:\/\/(www\.|)(portabletext\.org|sanity\.io)\/.*/gi,
                    {
                      name: 'internal url',
                      invert: true
                    }
                  ).warning(
                    `This is not an external link. Consider using internal links instead.`
                  )
              }
            ]
          },
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            fields: [
              {
                name: 'reference',
                type: 'reference',
                to: [
                  { type: 'post' }
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

The regular expression /https:\/\/(www\.|)(portabletext\.org|sanity\.io)\/.*/i triggers on all URLs that match all the variations of either portabletext.org or sanity.io with some sub-paths (this allows linking to the root domain).

A validation warning for the link annotation in the editor.

Margin markers

Gotcha

We recommend avoiding margin markers. Instead, use form components and the Form API.

Margin markers are still supported but will be deprecated in a future release.

In addition to validation markers, you can also define custom markers displayed in the right margin. You define these markers with the editor's input prop called markers.

Margin markers are objects like this:

{
  type: 'comment',
  path: [{_key: 'theBlockKey'}],
  item: {
    _type: 'comment',
    _key: '2f323432r23',
    body: 'I am commenting this block!'
  }
}

To show a custom marker, you must wrap the editor component and create a function that renders the custom markers as needed, and returns a React node (or null):

// renderCustomMarkers.js
import React from 'react'

export default function renderCustomMarkers(markers) {
  return (
    <div>
      {markers.map((marker, index) => {
        if (marker.type === 'comment') {
          return <div key={`marker${index}`}>A comment!</div>
        }
        return null
      })}
    </div>
  )
}

Then, create a wrapper for the editor input to give the editor the renderCustomMarker prop:

// CustomRichTextEditor.js
import React from 'react'
import {PortableTextInput} from 'sanity'
import renderCustomMarkers from './renderCustomMarkers' // From above example

function ArticleBlockEditor (props) {
  const {value, markers} = props
  const customMarkers = [
      {type: 'comment', path: value && value[0] ? [{_key: value[0]._key}] : [], value: 'This must be written better!'}
    ]
  const allMarkers = markers.concat(customMarkers) // [...markers, ...customMarkers] works too

  return (
    <BlockEditor
      {...props}
      markers={allMarkers}
      renderCustomMarkers={renderCustomMarkers}
    />
  )
}

export default ArticleBlockEditor

Finally, update the schema to use your wrapper component as inputComponent:

// content.js
import CustomRichTextEditor from './CustomRichTextEditor.js'

export default {
  title: 'Content',
  name: 'content',
  type: 'array',
  inputComponent: CustomRichTextEditor,
  of: [
    {type: 'block'}
  ]
}

Margin actions

You can also define actions for the blocks; actions also render in the right margin, beside markers. Actions render as buttons that initiate an action on the corresponding block. The pattern is the same as custom markers above, the only difference being that the prop key is renderBlockActions. It's a function that gets the block as input, and then returns a React node (or null) for that block.

The following example defines a margin action that renders a button that inserts a new block with a span that contains the text ”Pong!”:

// marginActions.js
import React from 'react'

function MarginActions (props) {

  const handleClick = event => {
    const {insert} = props
    insert([{
      _type: 'block',
      children: [
        {
          _type: 'span',
          text: 'Pong!'
        }
      ]
    }])
  }
  return (
    <button type="button" onClick={handleClick}>Ping</button>
  )
}

export default MarginActions  
  

To add the action to the custom rich text editor, import it and add it as a property:

// CustomRichTextEditor.js
import React from 'react'
import {BlockEditor} from 'sanity'
import renderCustomMarkers from './renderCustomMarkers'
import marginActions from './marginActions.js'

function ArticleBlockEditor (props) {
  const {value, markers = []} = props
  const customMarkers = [
      {type: 'comment', path: value && value[0] ? [{_key: value[0]._key}] : [], value: 'Rephrase this section for clarity!'}
    ]
  const allMarkers = markers.concat(customMarkers) // [...markers, ...customMarkers] works too

  return (
    <BlockEditor
      {...props}
      markers={allMarkers}
      renderCustomMarkers={renderCustomMarkers}
      renderBlockActions={marginActions}
    />
  )
}

export default ArticleBlockEditor

Further reading

Was this article helpful?