How to implement split letter animations on block content text in React.
This is a tricky challenge! When you split text into individual letter spans, you're essentially breaking apart the text nodes that marks and styles are applied to. Here's an approach that should preserve both marks and styles:
The key is to create a custom mark serializer that handles the letter splitting within the mark component itself, rather than splitting the text before serialization. This way, marks stay applied to their respective letters.
Here's a pattern that should work with @portabletext/react:
import { PortableText } from '@portabletext/react'
// Helper function to split text into letter spans
const splitIntoLetters = (text) => {
return text.split('').map((char, i) => (
<span key={i} className="letter" style={{ display: 'inline-block' }}>
{char === ' ' ? '\u00A0' : char}
</span>
))
}
// Recursively process children to split text nodes
const wrapChildrenWithSpans = (children) => {
return React.Children.map(children, child => {
if (typeof child === 'string') {
return splitIntoLetters(child)
}
return child
})
}
// Custom block serializer that preserves styles
const customBlock = ({ children, value }) => {
const style = value.style || 'normal'
const styleMap = {
h1: 'h1',
h2: 'h2',
normal: 'p',
// ... other styles
}
const Tag = styleMap[style] || 'p'
return <Tag className="split-text">{children}</Tag>
}
// Custom mark serializers that preserve formatting
const customMarks = {
strong: ({ children }) => (
<strong>{wrapChildrenWithSpans(children)}</strong>
),
em: ({ children }) => (
<em>{wrapChildrenWithSpans(children)}</em>
),
link: ({ children, value }) => (
<a href={value.href}>{wrapChildrenWithSpans(children)}</a>
),
// Add other custom marks as needed
}
// Use with PortableText
<PortableText
value={content}
components={{
block: customBlock,
marks: customMarks,
}}
/>The crucial part is the wrapChildrenWithSpans function that recursively processes children. When a mark serializer receives its children, it wraps any string content with letter spans while preserving the mark structure.
This means a bold, italic word will render as:
<strong><em><span>H</span><span>e</span><span>l</span><span>l</span><span>o</span></em></strong>For your GSAP/Framer Motion animations, you can now target .letter spans while maintaining the complete mark hierarchy. The animations will work on individual letters, but the styling from marks (bold, italic, links, etc.) stays intact.
Important tips:
- Use
\u00A0(non-breaking space) for spaces to prevent them from collapsing - Set
display: inline-blockon letter spans so transforms work properly in animations - Consider adding
white-space: preto the parent if you need to preserve multiple spaces
This approach works because you're splitting at the serialization level rather than preprocessing the Portable Text data. Each mark component handles its own text splitting, so the mark and style hierarchy defined in your Portable Text schema is fully preserved.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.