Style Sanity Portable Text blocks with the Tailwind .prose class
A Blocks component that will allow you to split rendering of normal text blocks (`_type==='block'`) like `h1`, `h2`, `p`, etc, and modules.
This was inspired by the ProseableText guide, which I found while looking for a good solution.
Tailwind's typography plugin is a great way of quickly getting a base level of styling applied to a piece of text, with easy ways to customize individual elements as well. Naively, you could use it by wrapping your <PortableText>
component in a <div className="prose">
.
However, there are several issues with that:
- It will apply styling to everything, e.g. also paragraphs, headings etc that are inside custom modules
- The first-child styles that remove top margin don't work on blocks of text after a custom module
The aforementioned guide states that the first issue now could be solved by slapping not-prose
on your custom modules. However, that will disable you from using it again in that module if you wanted. For example, I have a custom module called "image with text", where I want the text to be formatted with .prose again — but this is impossible once there is a not-prose
somewhere above in the DOM tree.
The solution I have built is based on the approach from <ProseableText>
, but I simplified it a bit and made it recursive. That way blocks of text inside modules are automatically wrapped in a <div className="prose">
container again, giving both styling and working first-child
styles.
Here you go, this is blocks/index.tsx
:
import { PortableText } from '@portabletext/react'
import { PortableTextBlock } from '@portabletext/types'
import { EncodeDataAttributeCallback } from '@sanity/react-loader'
import SectionImage from './section-image'
type Props = {
value: PortableTextBlock[]
sanity?: EncodeDataAttributeCallback
}
export default function Blocks({ value, sanity }: Props) {
let div: PortableTextBlock[] = []
return value.map((block, i, blocks) => {
// Normal text blocks (p, h1, h2, etc.) — these are grouped so we can wrap them in a prose div
if (block._type === 'block') {
div.push(block)
// If the next block is also text, group it with this one
if (blocks[i + 1]?._type === 'block') return null
// Otherwise, render the group of text blocks we have
const value = div
div = []
return (
<div key={block._key} className="prose-lg prose-h2:max-w-[32ch] prose-h2:text-5xl">
<PortableText
value={value}
components={{
marks: {
// ...
},
}}
/>
</div>
)
} else {
// Non-text blocks (modules, sections, etc.) — note that these can recursively render text
// blocks again
return (
<PortableText
key={block._key}
value={block}
components={{
types: {
'section.image': ({ value }) => <SectionImage {...value} sanity={sanity} />,
// ...
},
}}
/>
)
}
})
}
and as an example, here is the section module (blocks/section-image.tsx
) — simplified a bit but to demonstrate the recursiveness:
import { PortableTextBlock } from '@portabletext/types'
import { SanityImageObject } from '@sanity/image-url/lib/types/types'
import { EncodeDataAttributeCallback } from '@sanity/react-loader'
import { image } from 'lib/sanity/client'
import Blocks from '.'
type Props = {
image: {
image: SanityImageObject
}
richtext: PortableTextBlock[]
_key: string
sanity?: EncodeDataAttributeCallback
}
export default function SectionImage({ image: source, richtext, _key, sanity }: Props) {
const hotspot = source.image.hotspot
? { objectPosition: `${source.image.hotspot.x * 100}% ${source.image.hotspot.y * 100}%` }
: undefined
return (
<section className="...">
<div className="..." data-sanity={sanity?.([`body:${_key}`, 'image', 'image'])}>
<img
src={image(source.image)}
width={400}
height={1000}
className="absolute h-full w-full object-cover"
style={hotspot}
/>
</div>
<div className="...">
<Blocks value={richtext} />
</div>
</section>
)
}
Sanity – build remarkable experiences at scale
Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.