Using Sanity Portable Text for Rich-Text Fields
Learn how to define a Portable Text field in Sanity, see a schema and JSON example, render it with a React serializer, and avoid common pitfalls.
12 Aug 2025, 21:54 UTC

Why use Portable Text for rich text in Sanity
When you need editors to add formatted text, lists, or inline marks (bold, italic, code) without giving them raw HTML, define a Portable Text field. The field stores an array of block objects, each describing a paragraph or heading, its style, list item type, and any mark definitions. This keeps the content portable across platforms and lets you control the output with a serializer.
Worked configuration and example
Schema definition
Add a field of type array whose of array contains a single {type: 'block'} entry. The following snippet shows a minimal blog post schema:
// schemas/blog.js
export default {
name: 'blog',
title: 'Blog Post',
type: 'document',
fields: [
{name: 'title', title: 'Title', type: 'string'},
{
name: 'body',
title: 'Body',
type: 'array',
of: [{type: 'block'}],
},
],
}
After running sanity start the Studio presents a WYSIWYG editor for the body field. Saving a document with the text 'Hello world' produces a Portable Text value similar to:
[
{
_type: 'block',
style: 'normal',
markDefs: [
{_type: 'markDef', type: 'strong'}
],
children: [
{_type: 'span', text: 'Hello ', marks: []},
{_type: 'span', text: 'world', marks: ['strong']}
]
}
]
Rendering with a serializer
In a React application import @portabletext/react and provide a serializers object that maps block styles and mark types to JSX:
import PortableText from '@portabletext/react'
import { useSanityClient } from '@sanity/client'
function BlogPost({post}) {
const serializers = {
types: {
block: ({style, children}) => {
const tag = style === 'h1' ? 'h1' : style === 'h2' ? 'h2' : 'p'
return {children}
},
span: ({children, marks}) => {
// simple example: strong → , em →
return marks.reduce((acc, mark) => {
if (mark === 'strong') return {acc}
if (mark === 'em') return {acc}
return acc
}, children)
}
}
}
return
}
When the component renders, the Portable Text array is transformed into HTML‑like JSX, preserving bold and heading styles.
Limits and practical checks
- Declared block types only - The editor accepts only the types listed in the
ofarray. Adding a custom block (e.g., a{type: 'image'}block) requires you to include it there; otherwise the block is stripped on save unless you setallowUnknown: true(not recommended for production). - Mark definitions must be paired with serializers - If you use a mark like
codeorunderlinebut do not provide a serializer for it, the mark information is lost in the output. - Immutability - Portable Text values are plain JSON objects; mutating them directly can break the schema expected by the Studio or the serializer. Use helpers from
portable-text-utilsto insert, delete, or modify blocks.
How to verify the setup
- Check the schema file (
schemas/blog.js) for the field definition shown above. - Start the Studio (
sanity start) and confirm thebodyfield renders the rich-text editor. - Publish a document, then query the CDN:
https://<project>.api.sanity.io/v2023-05-03/data/query/<dataset>?query=*[_type==%22blog%22]{body}The response should return a
bodyarray whose elements have_type: 'block', astylestring, optionalmarkDefs, and achildrenarray of span objects. - In your React app, render the Portable Text value with the serializer block shown earlier and inspect the output in the browser; bold text should appear as
<b>(or<strong>depending on your mapper) and headings as<h1>or<h2>.
Common mistakes to avoid
- Omitting
{type: 'block'}from theofarray - the Studio will show a plain string field and any rich-text input will be lost. - Using a plain
stringortextfield for rich text - you lose the ability to represent lists, headings, or inline marks. - Forgetting to add serializers for marks such as
strong,em, orcode- the marks are stored but not visible in the rendered output. - Directly mutating Portable Text objects (e.g.,
post.body[0].children.push(...)) - this can create structures that do not match the schema and cause errors when the document is reloaded in the Studio.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.