The official editor for editing Portable Text – the JSON based rich text specification for modern content editing platforms.
Get started with the Portable Text Editor
This library provides you with the building blocks to create a completely custom editor experience built on top of Portable Text. We recommend checking out the official documentation. The following guide includes the basics to get your started.
In order to set up an editor you'll need to:
Create a schema that defines the rich text and block content elements.
Create a toolbar to toggle and insert these elements.
Write render functions to style and display each element type in the editor.
Render the editor.
Check out example application in this repo for a basic implementation of the editor. Most of the source code from this example app can also be found in the instructions below.
Next, in your app or the component you're building, import EditorProvider, PortableTextEditable, defineSchema, EventListenerPlugin, and the types in the code below.
Before you can render the editor, you need a schema. The editor schema configures the types of content rendered by the editor.
We'll start with a schema that includes some common rich text elements.
[!NOTE]
This guide includes a limited set of schema types, or rich text elements, to get you started. See the rendering guide for additional examples.
// App.tsx// ...const schemaDefinition = defineSchema({
// Decorators are simple marks that don't hold any datadecorators: [{name: 'strong'}, {name: 'em'}, {name: 'underline'}],
// Styles apply to entire text blocks// There's always a 'normal' style that can be considered the paragraph stylestyles: [
{name: 'normal'},
{name: 'h1'},
{name: 'h2'},
{name: 'h3'},
{name: 'blockquote'},
],
// The types below are left empty for this example.// See the rendering guide to learn more about each type.// Annotations are more complex marks that can hold data (for example, hyperlinks).annotations: [],
// Lists apply to entire text blocks as well (for example, bullet, numbered).lists: [],
// Inline objects hold arbitrary data that can be inserted into the text (for example, custom emoji).inlineObjects: [],
// Block objects hold arbitrary data that live side-by-side with text blocks (for example, images, code blocks, and tables).blockObjects: [],
})
Learn more about the different types that exist in schema in the Portable Text Overview.
Render the editor
With a schema defined, you have enough to render the editor. It won't do much yet, but you can confirm your progress.
Add useState from React, then scaffold out a basic application component. For example:
// app.tsximport {
defineSchema,
EditorProvider,
PortableTextEditable,
} from'@portabletext/editor'importtype {
PortableTextBlock,
RenderDecoratorFunction,
RenderStyleFunction,
} from'@portabletext/editor'import {EventListenerPlugin} from'@portabletext/editor/plugins'import {useState} from'react'const schemaDefinition = defineSchema({
/* your schema from the previous step */
})
functionApp() {
// Set up the initial state getter and setter. Leave the starting value as undefined for now.const [value, setValue] = useState<Array<PortableTextBlock> | undefined>(
undefined,
)
return (
<><EditorProviderinitialConfig={{schemaDefinition,
initialValue:value,
}}
><EventListenerPluginon={(event) => {
if (event.type === 'mutation') {
setValue(event.value)
}
}}
/>
<PortableTextEditable
// Addanoptionalstyletoseeitmoreeasilyonthepagestyle={{border: '1pxsolidblack', padding: '0.5em'}}
/></EditorProvider></>
)
}
exportdefaultApp
Include the App component in your application and run it. You should see an outlined editor that accepts text, but doesn't do much else.
Create render functions for schema elements
At this point the PTE only has a schema, but it doesn't know how to render anything. Fix that by creating render functions for each property in the schema.
Start by creating a render function for styles.
constrenderStyle: RenderStyleFunction = (props) => {
if (props.schemaType.value === 'h1') {
return<h1>{props.children}</h1>
}
if (props.schemaType.value === 'h2') {
return<h2>{props.children}</h2>
}
if (props.schemaType.value === 'h3') {
return<h3>{props.children}</h3>
}
if (props.schemaType.value === 'blockquote') {
return<blockquote>{props.children}</blockquote>
}
return<>{props.children}</>
}
Render functions all follow the same format.
They take in props and return JSX elements.
They use the schema to make decisions.
They return JSX and pass children as a fallback.
With this in mind, continue for the remaining schema types.
Create a render function for decorators.
constrenderDecorator: RenderDecoratorFunction = (props) => {
if (props.value === 'strong') {
return<strong>{props.children}</strong>
}
if (props.value === 'em') {
return<em>{props.children}</em>
}
if (props.value === 'underline') {
return<u>{props.children}</u>
}
return<>{props.children}</>
}
[!NOTE]
By default, text is rendered as an inline span element in the editor. While most render functions return a fragment (<>) as the fallback, make sure block level elements return blocks, like <div> elements.
Update the PortableTextEditable with each corresponding function to attach them to the editor.
You may notice that we skipped a few types from the schema. Declare these inline in the configuration, like in the code below. You can learn more about customizing the render functions in the documentation.
Before you can see if anything changed, you need a way to interact with the editor.
Create a toolbar
A toolbar is a collection of UI elements for interacting with the editor. The PTE library gives you the necessary hooks to create a toolbar however you like. Learn more about creating your own toolbar in the documentation.
Create a Toolbar component in the same file.
Import the useEditor hook, and declare an editor constant in the component.
Iterate over the schema types to create toggle buttons for each style and decorator.
Send events to the editor to toggle the styles and decorators whenever the buttons are clicked.
The useEditor hook gives you access to the active editor. send lets you send events to the editor. You can view the full list of events in the Behavior API reference.
[!NOTE]
The example above sends a focus event after each action. Normally when interacting with a button, the browser removes focus from the text editing area. This event returns focus to the field to prevent interrupting the user.
Bring it all together
With render functions created and a toolbar in place, you can fully render the editor. Add the Toolbar inside the EditorProvider.
You can now enter text and interact with the toolbar buttons to toggle the styles and decorators. These are only a small portion of the types of things you can do. Check out the custom rendering guide and the toolbar customization guide for options.
View the Portable Text data
You can preview the Portable Text from the editor by reading the state. Add the following after the EditorProvider.
In order to provide a robust and consistent end-user experience, the editor is backed by an elaborate E2E test suite generated from a human-readable Gherkin spec.
Development
Develop Together with Sanity Studio
Run pnpm build:editor to make sure it builds correctly
Now run pnpm dev:editor to run it in dev mode
In another terminal, open your local version of the sanity monorepo
cd into the sanity package and run pnpm link <relative path to the **editor** package in this repo>
Now, you should be able to run pnpm dev:test-studio in the sanity repo to test Studio with a locally running Portable Text Editor.
The npm package @portabletext/editor receives a total of 88,981 weekly downloads. As such, @portabletext/editor popularity was classified as popular.
We found that @portabletext/editor demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.It has 9 open source maintainers collaborating on the project.
Package last updated on 31 Jan 2025
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Socket is joining TC54 to help develop standards for software supply chain security, contributing to the evolution of SBOMs, CycloneDX, and Package URL specifications.
Malicious npm package postcss-optimizer delivers BeaverTail malware, targeting developer systems; similarities to past campaigns suggest a North Korean connection.
By Kirill Boychenko, Peter van der Zee - Jan 29, 2025