Add Portable Text Editor plugins to Studio

Use official and community-built Portable Text Editor plugins in your studio.

Sanity Studio uses the standalone Portable Text Editor (PTE) to power the block content editing experience. This makes the editor aware of your schema, and also lets you modify the editor from your existing Sanity config and schema.

Because the PTE itself doesn't require Studio, there can be times where you need to include a PTE plugin that isn't set up to work with Studio.

This guide shows you how to use an existing PTE plugin with your studio.

Prerequisites:

  • sanity version 3.92.0 or later is required for Studio to apply PTE plugins.
  • The PTE plugin must support the @portabletext/editor version that your sanity package uses. To check, run npm ls @portabletext/editor and compare the result with the plugin's @portabletext/editor peer dependency. For example, @portabletext/plugin-character-pair-decorator 9.x requires @portabletext/editor 8.x.

Add the plugin to your project

The first step is to add the PTE plugin to your project. The examples in this guide use the CharacterPairDecoratorPlugin from the Portable Text Editor repository. It lets you define a Markdown-style shortcut and link it to a decorator.

npm install @portabletext/plugin-character-pair-decorator

The examples configure the plugin to make any text wrapped in # bold. For example, #example# receives the "strong" decorator.

Option 1: Add the plugin directly

The most straightforward approach is to import the plugin and add it directly to your studio config for global use, or your schema for specific component use.

Note the change to TSX

Apply the plugin to every Portable Text field

This applies the plugin to all block content editors throughout your studio.

import { defineConfig } from 'sanity'
import { CharacterPairDecoratorPlugin } from '@portabletext/plugin-character-pair-decorator'

export default defineConfig({
  // ... omitted for brevity
  form: {
    components: {
      portableText: {
        plugins: (props)=>{
          return (
            <>
              {props.renderDefault(props)}
              <CharacterPairDecoratorPlugin 
                decorator={({context}) =>
                  context.schema.decorators.find((d) => d.name === 'strong')?.name
                }
                pair={{char: '#', amount: 1}}
              />
            </>
          )
        },
      }
    },
  }
})

Apply the plugin to one field

This applies the plugin to a single block content field in your schema.

import { defineType } from 'sanity'
import { CharacterPairDecoratorPlugin } from '@portabletext/plugin-character-pair-decorator'

export const post = defineType({
  title: 'Blog post',
  name: 'post',
  type: 'document',
  fields: [
    // ...
    {
      type: 'array',
      name: 'content',
      title: 'Post Body',
      of: [
        {
          type: 'block',
        }
      ],
      components: {
        portableText: {
          plugins: (props)=>{
            return (
              <>
                {props.renderDefault(props)}
                <CharacterPairDecoratorPlugin 
                  decorator={({context}) =>
                    context.schema.decorators.find((d) => d.name === 'strong')?.name
                  }
                  pair={{char: '#', amount: 1}}
                />
              </>
            )
          }
        }
      }
    },
    // ...
  ],
})

Option 2: Create a container for plugins

This is the recommended approach. Wrap all editor plugins in a single container component, so your schema and configuration files don't need to change to TSX. For example, the guide on creating your own PTE plugin exports a PortableTextEditorPlugins function.

import type { PortableTextPluginsProps } from 'sanity'
import { CharacterPairDecoratorPlugin } from '@portabletext/plugin-character-pair-decorator'

export function PortableTextEditorPlugins(props: PortableTextPluginsProps) {
  return (
    <>
      {props.renderDefault(props)}
      <CharacterPairDecoratorPlugin 
        decorator={({context}) =>
          context.schema.decorators.find((d) => d.name === 'strong')?.name
        }
        pair={{char: '#', amount: 1}}
      />
      {/* Add more plugins here  */}
    </>
  )
}

Then use that container globally in the sanity.config.ts, or locally in your schema.

import { defineConfig } from "sanity"
import { PortableTextEditorPlugins } from './plugins/pte'

export default defineConfig({
  // ...
  form: {
    components: {
      portableText: {
        plugins: PortableTextEditorPlugins,
      },
    },
  },
  // ...
})

Option 3: Wrap in a Studio plugin

One approach is to wrap the PTE plugin in a Sanity Studio plugin using definePlugin, then add it to your config file as a plugin.

import { definePlugin } from 'sanity'
import { CharacterPairDecoratorPlugin } from '@portabletext/plugin-character-pair-decorator'

export const characterPair = definePlugin({
  name: 'characterPair',
  form: {
    components: {
      portableText: {
        plugins: (props)=>{
          return (
            <>
              {props.renderDefault(props)}
              <CharacterPairDecoratorPlugin 
                decorator={({context}) =>
                  context.schema.decorators.find((d) => d.name === 'strong')?.name
                }
                pair={{char: '#', amount: 1}}
              />
            </>
          )
        },
      }
    },
  }
})

If you package a plugin this way for distribution, expose its settings as plugin options so users can configure it in their sanity.config.ts. To do this, give definePlugin an options type, and return the plugin configuration from a function that receives those options:

import { definePlugin } from 'sanity'
import { CharacterPairDecoratorPlugin } from '@portabletext/plugin-character-pair-decorator'

interface CharacterPairOptions {
  decorator?: string
  char?: string
}

export const characterPair = definePlugin<CharacterPairOptions | void>((options) => {
  // Defaults: the strong decorator and the # character
  const { decorator = 'strong', char = '#' } = options || {}

  return {
    name: 'characterPair',
    form: {
      components: {
        portableText: {
          plugins: (props) => (
            <>
              {props.renderDefault(props)}
              <CharacterPairDecoratorPlugin
                decorator={({context}) =>
                  context.schema.decorators.find((d) => d.name === decorator)?.name
                }
                pair={{char, amount: 1}}
              />
            </>
          ),
        },
      },
    },
  }
})

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme