Studio

Configure the Portable Text Editor

Configure the Portable Text Editor: styles, lists, decorators, annotations, custom blocks, tables, and the built-in Markdown and typography behaviors.

This page covers how to configure the Portable Text Editor (PTE) in Sanity Studio: which styles, lists, decorators, and annotations editors can apply, which custom blocks they can insert, and how to turn the editor's built-in Markdown, typography, table, and paste-link behaviors on or off.

Portable Text is extensible. Each block can have a style and a set of mark definitions that describe data structures distributed in the child spans. It also enables inserting arbitrary data objects in the array, requiring only a _type key. In a Sanity document, array members also need a _key, which the editor generates for you if it is missing. Portable Text allows custom content objects in the root array, enabling editing and rendering environments to mix rich text with custom content types.

You can create as many versions of the PTE as you want. A frequent pattern is a base configuration with selected decorators and annotations, and a more comprehensive configuration with custom block types.

This is helpful if editors only use emphasis and annotate text as internal links in some settings (for example, a caption) and have a full toolbox available in another (for example, an article body).

Minimal configuration

The following example shows a minimal configuration to implement Portable Text and the editor in Sanity Studio:

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

The code, an array of blocks, renders the PTE with a default configuration for styles, decorators, and annotations.

Loading...

Portable Text is markup-agnostic; however, the default configuration maps easily to HTML conventions. Bold and italics set the decorators strong and em (emphasis), and produce a data structure like this:

[
  {
    "_type": "span",
    "_key": "eab9266102e81",
    "text": "strong",
    "marks": [
      "strong"
    ]
  },
  {
    "_type": "span",
    "_key": "eab9266102e82",
    "text": " and ",
    "marks": []
  },
  {
    "_type": "span",
    "_key": "eab9266102e83",
    "text": "emphasis.",
    "marks": [
      "em"
    ]
  }
]

Portable Text isn't designed for direct human authoring or reading, but instead should be parsed by software. The _type key also makes it queryable in Sanity's APIs, and by other JSON tools, such as jq or groq-js.

Default behaviors

Markdown behaviors

The PTE in Studio includes many default Markdown behaviors that may be familiar from other text editors. They map to the standard styles, lists, decorators, and annotations. These include:

  • # Title: One or more # symbols followed by a space sets the matching heading level.
  • > block quote content: The > character followed by a space and text converts to a block quote.
  • Backspace at the beginning of a styled block removes its style.
  • -, *, or 1.: At the beginning of a line, these characters start a list.
  • `code`: Wrapping text in single backticks creates inline code.
  • *italic* or _italic_: Wrapping text in single asterisks or underscores creates italic text.
  • **bold** or __bold__: Wrapping text in double asterisks or underscores creates bold text.
  • ~~strikethrough~~: Wrapping text in double tildes creates strikethrough text.

You can undo any of these transformations by pressing Backspace immediately after it happens. Moving the cursor first commits the change.

Disable default Markdown behavior

You can disable these Markdown behaviors by modifying the configuration, either in your sanity.config.ts file globally, or on individual schema types.

If you're already using custom PTE behavior plugins, you can add the contents of props.renderDefault above into the renderDefault call in the behavior plugin.

Typographic behaviors

The editor also includes a set of common typographic helpers, available from Studio v4.16.0 and enabled by default from v5.0.0. These transform the input inline, saving the transformed version to the document.

Each entry below lists the behavior name, the input text, and the output text. Use the behavior name to enable or disable an individual behavior in the configuration. The default behaviors apply to every PTE field unless you turn them off.

Default behaviors:

  • emDash: -- → —
  • ellipsis: ... → …
  • openingDoubleQuote: " → “
  • closingDoubleQuote: " → ”
  • openingSingleQuote: ' → ‘
  • closingSingleQuote: ' → ’
  • leftArrow: <- → ←
  • rightArrow: -> → →
  • copyright: (c) → ©
  • trademark: (tm) → ™
  • servicemark: (sm) → ℠
  • registeredTrademark: (r) → ®

The four quote behaviors take the same straight-quote input. The editor chooses the opening or closing form based on the surrounding text.

Optional behaviors:

  • oneHalf: 1/2 → ½
  • plusMinus: +/- → ±
  • notEqual: != → ≠
  • laquo: << → «
  • raquo: >> → »
  • multiplication: * or x between numbers → ×
  • superscriptTwo: ^2 → ²
  • superscriptThree: ^3 → ³
  • oneQuarter: 1/4 → ¼
  • threeQuarters: 3/4 → ¾

Enable or disable typography behaviors

You can enable or disable typography behaviors at the global level, or directly in the schema definition for the block.

Explicitly disable all typography behaviors:

Set the preset key to one of:

  • default: Enables the default behaviors, and applies when unset.
  • all: Enables both default and optional behaviors.
  • none: Disables both default and optional behaviors. Use this if you plan to enable only specific behaviors.

Enable or disable individual behaviors with the enable or disable key. Each accepts an array of behavior names.

Map Markdown behaviors to custom styles, lists, and decorators

If you're using non-standard names for your marks and decorators, you can map the default behaviors to the different names.

In this example, the unordered list is remapped to a list named "dot". Note that the schema declares the list with a value key while the callback reads list.name. The editor compiles value into name when it builds its own schema.

Paste link behavior

If your Studio uses the default link annotation, you can select text and paste a URL to annotate the text as a link. To disable this behavior, set the pasteLink.enabled value to false.

By default, the plugin looks for an annotation of name 'link' with an 'href' string field, and if that is present, it uses that to create the link. This behavior can be configured by providing a custom link matcher function (a function that has access to the current editor schema and returns either a typed object or undefined):

Table editing

The Portable Text Editor includes built-in table editing, powered by @portabletext/plugin-table. Table editing is available from Studio v6.6.0 and is disabled by default. When enabled, the editor renders table blocks as editable tables with row and column controls, plus a table menu with a header row toggle. Tables inserted from the insert menu start as a 3×3 grid with a header row.

Loading...

Enabling table editing takes two steps: declare the table schema the plugin binds to, and turn on the plugin.

Define the table schema

By default, the plugin binds to a canonical schema shape: a table object whose rows array holds row objects, each holding a cells array of cell objects, each with a value array of blocks. Declare a headerRows number field on the table as well. (If your dataset already has a table-shaped type under different names, see the "Use your own table type names" section below.)

Register the table type in your schema's types array, and add table to the of array of your Portable Text field so editors can insert it (shown in the next section).

Gotcha

Cells hold regular block content. You can extend the cell's value array with inline objects, images, or other custom members like any other Portable Text field.

Enable the table plugin

Enable the plugin in your sanity.config.ts file globally, or on individual schema types.

Use your own table type names

If your dataset already contains a table-shaped type, bind the plugin to it instead of migrating data. The containers option takes defineContainer definitions for the table, row, and cell roles, where the type and array field names are yours. defineContainer comes from @portabletext/editor, which you add as a dependency.

Pass them alongside enabled, in either the global or the individual setup shown above:

plugins: (props) => {
  return props.renderDefault({
    ...props,
    plugins: {
      ...props.plugins,
      table: {
        enabled: true,
        containers: tableContainers
      }
    }
  })
}

Roles you omit fall back to the canonical names, and omitting containers entirely behaves exactly like the canonical setup above. Any fields on your types that table editing doesn't manage persist untouched, as long as your schema declares them. Each container definition also accepts a custom render; note that providing one for the table role replaces the Studio's table UI, including its table menu and header row toggle. If your render still uses the plugin's own table component without supplying a menu, the plugin's built-in menu renders in its place.

Add custom blocks

Since Portable Text defines block content as an array, adding custom content blocks for images, videos, or code embeds means inserting these items between paragraph blocks.

Gotcha

Example: images

To add images to Portable Text, append a new type object to the array:

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

This configuration adds an insert menu with Image as the only option:

Loading...
The insert image option is added to the toolbar

Selecting an image inserts the block with a preview in the Portable Text Editor. You can drag the image and drop it in its designated position; you can also edit it by double-clicking the preview box or by selecting the edit option from the context menu:

Loading...
Edit or delete a custom block

The Portable Text data structure for this example looks like the following:

[
  {
    "style": "normal",
    "_type": "block",
    "markDefs": [],
    "_key": "09cc5f099d3b",
    "children": [
      {
        "_type": "span",
        "_key": "09cc5f099d3b0",
        "text": "Kokos is a miniature schnauzer.",
        "marks": []
      }
    ]
  },
  {
    "_type": "image",
    "_key": "a5e9155ee3f5",
    "asset": {
      "_type": "reference",
      "_ref": "image-61991cfbe9182124c18ee1829c07910faadd100e-2048x1366-png"
    }
  },
  {
    "style": "normal",
    "_type": "block",
    "markDefs": [],
    "_key": "54145e9cb006",
    "children": [
      {
        "_type": "span",
        "_key": "54145e9cb0060",
        "text": "Kokos is a good dog!",
        "marks": []
      }
    ]
  }
]

The image is its own object, where asset references the asset document. You can derive the image URL from the asset's _id (which matches the _ref value above), though most projects use the @sanity/image-url builder instead. You can also join the asset document with a conditional projection:

*[_type == "post"]{
  ...,
  content[]{
    ...,
    _type == "image" => {
      ...,
      asset->
    }
  }
}

Example: code input

Our documentation features many code blocks. They are custom blocks that we added to our editor. Install the code input plugin with npm, or with the sanity install CLI command:

Once installed and configured, you can add the code block to your Portable Text Editor configuration:

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

Code is now available as a selection in the insert menu. To change the label, add title: 'My title' to the same object. Inserting a code block produces a preview and a code editor:

Loading...
The code editor with some schema code in JavaScript

You can set more options for the code input.

Configure styles for text blocks

Out of the box, the Portable Text Editor includes the following styles: normal, h1 through h6, and blockquote. By default, they map to HTML, but a style can be an arbitrary value. The normal style is always available, because the editor prepends it if your styles array omits it.

// The default set of styles
export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'Heading 1', value: 'h1'},
        {title: 'Heading 2', value: 'h2'},
        {title: 'Heading 3', value: 'h3'},
        {title: 'Heading 4', value: 'h4'},
        {title: 'Heading 5', value: 'h5'},
        {title: 'Heading 6', value: 'h6'},
        {title: 'Quote', value: 'blockquote'}
      ]
    }
  ]
}
Loading...
The default style configuration in the editor

We recommend keeping the configuration reasonably abstract and following established conventions. If you plan to render with Sanity's Portable Text tooling, stay close to HTML naming conventions.

To override the default configuration for styles, add the styles key and set an array of title/value objects:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        { title: 'Normal', value: 'normal' },
        { title: 'Heading 2', value: 'h2' },
        { title: 'Quote', value: 'blockquote' },
        { title: 'Hidden', value: 'blockComment' }
      ]
    }
  ]
}

Here we have set four possible styles. The first three are from the default settings and are parsed in HTML to <p>, <h2>, and <blockquote>.

blockComment is an arbitrary style that we set because we plan to make it possible for editors to hide selected blocks of text from rendering, while keeping them available in the source code as block comments.

To change how blocks look inside the editor, see Customize the Portable Text Editor.

Loading...
Editor with style configuration

Configure lists for text blocks

The editor supports two types of lists: bullet (unordered) and number (ordered). If your block type doesn't contain a lists definition, your editor features both a bullet list and a numbered list option:

Loading...
Bullet and numbered lists

The default is the equivalent of explicitly naming both:

// The default set of lists
export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      lists: [
        {title: 'Bulleted list', value: 'bullet'},
        {title: 'Numbered list', value: 'number'}
      ] // yes please, both bullet and numbered
    }
  ]
}

You can override the default by naming the lists you want. To disable lists altogether, leave the array empty:

// No lists
export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      lists: [] // no lists, thanks
    }
  ]
}

You also decide what goes into the title: {title: 'Prioritized', value: 'number'} works equally well.

Configure marks for inline text

Portable Text enables marks to label inline text with additional data. There are two types of marks: decorators and annotations. Decorators are simple string values, while annotations are keys pointing to a data structure. Annotations are a powerful feature of Portable Text in combination with the Content Lake, because they allow embedding complex data structures and references in running text.

Decorators

Decorators work similarly to styles, but they apply to spans, that is, inline text. The defaults are strong, em, code, underline, and strike-through (which the toolbar labels "Strike"). To replace these with your own set, add an array to the decorators key, under marks:

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

Decorators are displayed as icons in the toolbar. This configuration looks like this:

Loading...
Toolbar with custom decorator configuration

Annotations

Annotations enable embedding rich content data in inline text. An example can be a reference to another document, typically used for internal linking.

To add an internal link annotation, configure the Portable Text schema like this:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          // ...
        ],
        annotations: [
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            fields: [
              {
                name: 'reference',
                type: 'reference',
                title: 'Reference',
                to: [
                  { type: 'post' },
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

Gotcha

Annotations without an icon are displayed in the toolbar as question mark icons. For more information on editing toolbar icons, see Customize the Portable Text Editor.

Loading...
Reference modal for internal link annotation

The corresponding Portable Text data structure looks like this:

[
  {
    "_key": "da9dc50335a0",
    "_type": "block",
    "children": [
      {
        "_key": "da9dc50335a00",
        "_type": "span",
        "marks": [
          "5b86c1132a66"
        ],
        "text": "This is an internal link"
      },
      {
        "_key": "da9dc50335a01",
        "_type": "span",
        "marks": [],
        "text": "."
      }
    ],
    "markDefs": [
      {
        "_key": "5b86c1132a66",
        "_type": "internalLink",
        "reference": {
          "_ref": "1dfa4e95-9f92-4e13-901b-1a769724e23c",
          "_type": "reference"
        }
      }
    ],
    "style": "normal"
  }
]

Was this page helpful?