Converters
Converter signature and patterns for rich text, media, page references, arrays, theme inheritance and inline blocks.
On this page
A converter turns a block's serialized Lexical node into JSX on the frontend. It receives the node and a nodesToJSX helper for nested editor content.
Converter signatureLink to this section
type CustomBlockConverter<T = any> = (props: {
node: SerializedBlockNode<T>
nodesToJSX: (args: { nodes: unknown[] }) => JSX.Element[]
}) => JSX.Element | nullWith rich text (nodesToJSX)Link to this section
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
export const sectionConverter: CustomBlockConverter = ({ node, nodesToJSX }) => {
const { theme, content, padding } = node.fields as {
theme?: string
content?: SerializedEditorState
padding?: number
}
const contentJSX = content?.root?.children
? nodesToJSX({ nodes: content.root.children })
: null
return (
<Section theme={theme} padding={padding}>
{contentJSX}
</Section>
)
}With media (uploads)Link to this section
Uploads come back as either the full document or just the ID, depending on depth — always check.
import type { Upload } from '@/payload-types'
const image = typeof fields.image === 'object' ? fields.image : null
if (!image?.url) return null
;<Image
src={image.url}
alt={image.alt || ''}
width={image.width || 0}
height={image.height || 0}
/>With page referencesLink to this section
import type { Page } from '@/payload-types'
const { linkType, url, reference, newTab } = node.fields as {
linkType?: 'custom' | 'internal'
url?: string
reference?: Page | number
newTab?: boolean
}
const href =
linkType === 'custom'
? url || ''
: reference && typeof reference === 'object' && 'fullPath' in reference
? reference.fullPath || '/'
: ''With array fieldsLink to this section
const { items } = node.fields as { items?: (Upload | number)[] }
items?.map((item, i) => {
if (typeof item !== 'object') return null
return (
<CarouselItem key={i}>
<Image src={item.url || ''} alt={item.alt || ''} />
</CarouselItem>
)
})Theme inheritanceLink to this section
Theme 'inherit' means use the parent's theme — pass undefined to skip applying a theme on this block.
<Section
theme={theme && theme !== 'inherit' ? theme : undefined}
layoutBackground={
theme && theme !== 'inherit' ? layoutBg : undefined
}
/>Block name → ID/ARIALink to this section
import { toUrlCase } from '@systhemaui/core'
// Non-interactive block wrappers expose `blockName` as a stable anchor id
// (and, for landmark containers, as an accessible label).
;<section
id={blockName ? toUrlCase(blockName) : undefined}
aria-label={blockName || undefined}
/>Inline blocks (button, icon)Link to this section
Both the built-in button and icon inline blocks share the same link-fields shape (linkType, url, reference, newTab). Render with the polymorphic *.a variant when a link resolves, plain when it doesn't:
import type { ButtonVariant } from '@systhemaui/core'
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'
import { resolveIconForRender } from '@systhemaui/payload/fields/iconPicker/resolveIconForRender'
export const buttonConverter: CustomBlockConverter = ({ node }) => {
const { variant, title, iconBefore, iconAfter, linkType, url, reference, newTab } =
node.fields as {
variant?: ButtonVariant
title?: string
iconBefore?: string
iconAfter?: string
linkType?: 'custom' | 'internal'
url?: string
reference?: Page | number
newTab?: boolean
}
const href = linkType === 'custom' ? url || '' : /* resolve reference */ ''
return (
<Button.a href={href} variant={variant} target={newTab ? '_blank' : undefined}>
{iconBefore && (
<ButtonIcon dangerouslySetInnerHTML={{ __html: resolveIconForRender(iconBefore).svg }} />
)}
{title && <ButtonTitle>{title}</ButtonTitle>}
{iconAfter && (
<ButtonIcon dangerouslySetInnerHTML={{ __html: resolveIconForRender(iconAfter).svg }} />
)}
</Button.a>
)
}The icon block ships its own converter — when a link resolves the converter renders <Icon.a> (Next-aware Link via @systhemaui/next); otherwise it renders plain <Icon> so the hover affordance only kicks in for actual links. The same shape covers the linkable Image / Video media blocks. The icon block stores its link fields in a collapsed-by-default "Link Settings" section in the Edit Icon modal, including an Accessible label field that becomes the linked icon's aria-label (a text-less linked icon otherwise has no accessible name); when empty, a name is derived from the link's domain. For the four-state resolver pattern that custom inline-block converters should mirror, see Linkable icons → custom inline-block converters.