Unlock seamless workflows and faster delivery with our latest releases – get the details

A block content editor that loves you back

The Portable Text Editor is a powerful, customizable editor for authoring rich text and block content. It can now be used in any project.

Published

  • Knut Melvær

    Knut Melvær

    Head of Developer Community and Education

  • Christian Grøngaard

    Christian Grøngaard

    Senior Software Engineer, Creator Experience

  • Mark Michon

    Mark Michon

    Documentation Specialist

An abstract 3D illustration features the word “TEXT” in bold blue capital letters on a white square surface. The square is emerging from a recessed platform, giving a sense of depth and dimensionality. The background is a light gray gradient, creating a clean and minimalistic design.

Ever spent hours wrangling a WYSIWYG editor that just doesn’t get you?

Building rich text editors is notoriously hard—especially when they need to support real-time collaboration, AI integrations, and massive documents while performing smoothly across browsers, devices, and locales.

Sanity Studio has always shipped with the Portable Text Editor, built on top of Slate.js, to let thousands of creators author structured, presentation-agnostic block content. But producing customizable data wasn’t enough—we also needed a format that avoids storing HTML strings while being easy to render across apps, websites, or even print.

Enter Portable Text: an open-source, JSON-based specification for block content. It works seamlessly with modern frameworks by letting you map content as data to components, thus eliminating awkward HTML injections and giving you full control over your content structure. In other words, it never makes you do silly things like passing a string of HTML into dangerouslySetInnerHTML.

Until now, Portable Text authoring has only been possible inside Sanity Studio. That changes today. The standalone Portable Text Editor is here, ready to integrate into any React-based project. In fact, we have been using the standalone Portable Text Editor extensively to create Sanity Create, your new free-form content authoring tool.

We’ve also included the beta of the Behavior API, which lets you hook into editor events and customize its… well, behavior. It’s powerful. It’s flexible. And you can start exploring it right now on the new documentation site on portabletext.org.

Keep reading to see what it can do—or dive into the docs and start building!

The standalone Portable Text Editor

The standalone Portable Text Editor is a React-based customizable block content editor that you can now use independently of Sanity Studio, enabling you to integrate rich text and block content authoring capabilities into any application that supports React components.

Install @portabletext/editor and compose your editor exactly how you need it. Here is a minimal example:

import {
  EditorProvider,
  EditorEventListener,
  PortableTextEditable,
} from "@portabletext/editor";
import { useState } from "react";

export function MinimalEditor() {
  const [value, setValue] = useState([]);

  return (
    <EditorProvider>
      <EditorEventListener
        on={(event) => {
          if (event.type === "mutation") {
            setValue(event.snapshot);
          }
        }}
      />
      <PortableTextEditable />
    </EditorProvider>
  );
}

At a high level, the editor consists of these parts:

  • <EditorProvider /> that takes a schema for your rich text and blocks content
  • <EditorEventListener /> that lets you access the editor's state
  • <PortableTextEditable /> provides the area that you can type into

In addition to this, the package gives you hooks that you can use to access its state:

import {useEditor} from '@portabletext/editor'

function Toolbar() {
  const editor = useEditor()
}

Below is an example to make a toolbar for decorators (for example, strong and emphasis):

import {useEditor} from '@portabletext/editor'
import {schemaDefinition} from './pt-schema'

function Toolbar() {
  const editor = useEditor()
  const decoratorButtons = schemaDefinition.decorators.map((decorator) => (
  <button
    key={decorator.name}
    onClick={() => {
      editor.send({
        type: 'decorator.toggle',
        decorator: decorator.name,
      })
      editor.send({type: 'focus'})
    }}
  >
    {decorator.name}
  </button>
))
}
A screenshot of a simple rich text editor interface. The text input area contains the sentence: “This is a simple rich text editor.” where “simple” is bold, “rich” is italicized, and “editor” is underlined. Above the text input are formatting options labeled Strong, Em, and Underline for bold, italic, and underline styling, respectively. Below the input area, there is a purple Submit button.
Add a bit of styling and you can have a rich text editor like this

Adding affordances for rich text formatting is a must. Where the Portable Text Editor shines is its flexibility in defining how it works with the new Behavior API.

Behavior API: Custom shortcuts, bespoke editing maneuvers, and precise paste handling

The Portable Text Editor also has a new Behavior API, currently in beta. It allows you to customize the editor's behavior to fit your specific use cases. This could include things like:

  • Defining custom keyboard shortcuts
  • Handling paste events to support pasting HTML, Markdown, or other formats
  • Add affordances like slash-commands, :emoji: shortcuts, and auto-completion of brackets

Below is an example of a behavior that auto-closes parenthesis and places your cursor in-between, like you are used to from code editors:

import {defineBehavior} from '@portabletext/editor/behaviors'

export const autoCloseParens = defineBehavior({
  on: 'insert.text',
  guard: ({context, event}) => {
    return event.text !== '('
  },
  actions: [
    ({event, context}) => [
      // Send the original event that includes the '('
      event,
      // Send a new insert.text event with a closing parenthesis
      {
        type: 'insert.text',
        text: ')',
      },
      // Send a select event to move the cursor in between the parens
      {
        type: 'select',
        selection: {
          anchor: {
            path: context.selection.anchor.path,
            offset: context.selection.anchor.offset + 1,
          },
          focus: {
            path: context.selection.focus.path,
            offset: context.selection.focus.offset + 1,
          },
        },
      },
    ],
  ],
})

The Behavior API treats the editor like a state machine. You'll recognize the patterns if you are familiar with the XState APIs.

We're excited about the new Behavior API's possibilities for tailoring the editor experience to your needs. While the API is currently in beta, we look forward to stabilizing it and leveraging it to enable even more powerful customization options for the Portable Text Editor for the Sanity Studio.

Gherkin and Racejar: Testing suite for the Portable Text Editor

Content editing reliability is no joke. Few things are more infuriating than a text editing interface that doesn't quite work how you expected or just fails you. That's why we have invested heavily in an extensive testing suite for the Portable Text Editor to ensure a reliable and consistent editing experience.

Our testing approach leverages the Gherkin syntax, a human-readable language for specifying test scenarios. To make it easier to set up these tests with modern web tooling, we developed Racejar, a powerful testing tool that allows us to define and run end-to-end tests for the editor with libraries like Jest and Vitest.

This combination enables us to:

  • Write test scenarios in plain English that are easy to understand and maintain
  • Automatically generate a comprehensive set of test cases from these scenarios
  • Run the tests against the editor to verify its behavior and catch any regressions

By continuously running this test suite, we can ensure that the Portable Text Editor delivers a rock-solid editing experience across various use cases, input methods, and edge cases. You can have confidence that the editor will work as expected, allowing you to focus on building great content-driven experiences.

The test for making sure that collaborative editing and undo/redo actions work as expected looks like this:

Feature: Undo/Redo Collaboration

  Background:
    Given two editors
    And a global keymap

  Scenario: Undoing deleting before remote text
    Given the text "hello world"
    When "Backspace" is pressed
    And the caret is put after "hello worl" by editor B
    And " there" is typed by editor B
    Then the text is "hello worl there"
    When undo is performed
    Then the text is "hello world there"

The Road Ahead: Bringing It All Back Home

With the standalone Portable Text Editor now available, we're curious to see what you build with it. And we're excited about bringing these innovations—including the new Behavior API and enhanced customization options—back to Sanity Studio in upcoming releases. Whether you're using the standalone editor in your React applications or working with the full power of Studio, our goal remains the same: making structured content editing more powerful and enjoyable. Try it out at portabletext.org, and let us know what you think!