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
Head of Developer Community and Education
Christian Grøngaard
Senior Software Engineer, Creator Experience
Mark Michon
Documentation Specialist
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>
))
}
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!