Sanity Portable Text: decorators vs block serializers in React
When Sanity rich text loses custom styles in your React app, the fix usually comes down to choosing the right Portable Text serializer.
11 Sept 2026, 21:27 UTC

A custom style that disappears between Studio and the browser
You add a "pull quote" decorator to a Sanity rich text field. Editors highlight a sentence, apply the style, and publish. On your website, that sentence renders as ordinary paragraph text. No error appears in the console. The style is simply gone.
The takeaway: Portable Text is not HTML, and Sanity does not render custom marks for you. Your frontend must explicitly map each mark to a React component. Choosing between a decorator and a block serializer determines whether that mapping happens at all.
What Portable Text actually stores
Portable Text is Sanity's JSON-based rich text format. A schema field of type array with of: [{ type: 'block' }] stores an ordered array of block objects. Each block contains children, an array of span objects. Spans hold the visible text and a marks array listing inline annotations such as bold, italic, or a custom key like pull-quote.
A block can also carry a style (for example, h2 or normal) and a listItem value. Because the format is an array of blocks rather than an HTML string, you cannot pass it directly to dangerouslySetInnerHTML and expect custom components to work.
Decorators versus block serializers
Use a decorator when an author applies a style to a range of text inside a paragraph. In the schema, decorators are declared with a title and value. The frontend serializer maps that value to an inline component such as span or mark.
Use a block serializer when the custom behavior applies to the entire block, such as rendering a callout as an aside instead of a p. Block serializers receive the whole block object and return a React element.
| Concern | Decorator | Block serializer |
|---|---|---|
| Scope | Inline range within a block | Entire block object |
| Schema key | decorators | styles or custom block type |
| Frontend key | serializers.marks | serializers.blocks |
| Typical output | span, mark, a | section, aside, div |
Worked example: rendering a pull-quote mark
First, confirm the schema uses Portable Text. Look for a field like this in your Sanity schema file:
export const body = {
name: 'body',
title: 'Body',
type: 'array',
of: [
{
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'Heading 2', value: 'h2' },
],
decorators: [
{ title: 'Pull quote', value: 'pull-quote' },
{ title: 'Highlight', value: 'highlight' },
],
lists: [{ title: 'Bullet', value: 'bullet' }],
},
],
};
Run this check in your Sanity Studio project directory. No special permissions are required to read the schema file.
Next, project the field with GROQ. Run this in Sanity Vision or against your dataset API:
*[_type == "post"][0] {
title,
body[] {
_type,
style,
markDefs[],
children[] {
_type,
text,
marks
}
}
}
Expected result: each item in body has _type: "block", a children array, and spans with a marks array. If marks contains pull-quote, the editor applied the decorator.
In your React or Next.js frontend, install @portabletext/react and register serializers:
import { PortableText } from '@portabletext/react';
const serializers = {
marks: {
pullQuote: (props) => ,
highlight: (props) => ,
},
};
export default function Post({ body }) {
return ;
}
Run this in your frontend application. You need read access to the Sanity dataset; use a public API token only if the dataset is not publicly readable. The placeholder body is the Portable Text array projected by your GROQ query.
Expected check: open browser developer tools on the rendered page and search for pull-quote or highlight. If the custom class is absent, the mark serializer is missing or the key does not match the schema value exactly. Sanity will not warn you about unhandled marks.
Limitations and verification
- Large Portable Text documents increase Studio load time and GROQ payload size. Project only the fields you need instead of returning the entire document.
- Nested block content or custom annotations require recursive serialization on the frontend.
- Sanity Studio major version matters. Serializer APIs differ between Studio v2 and v3. Check
package.jsonbefore adopting code patterns from documentation written for the other major version. - Custom marks are silently dropped if the frontend does not serialize them. Verify in the DOM, not only in Studio.
Practical verification: publish a post with the custom style applied, fetch it through your frontend route, and inspect the generated HTML for your serializer's class name or component wrapper.
Closing
Start with decorators for inline styles such as pull quotes and highlights. Reach for block serializers only when the entire block needs a different element. Treat Portable Text as structured data, not markup, and your custom styles will survive the trip from editor to browser.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.