---
title: "Converters"
description: "Converter signature and patterns for rich text, media, page references, arrays, theme inheritance and inline blocks."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/payload/custom-blocks/converters
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/llms.txt
---
> This page isn't translated yet. Showing English.


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 signature

```ts
type CustomBlockConverter<T = any> = (props: {
  node: SerializedBlockNode<T>
  nodesToJSX: (args: { nodes: unknown[] }) => JSX.Element[]
}) => JSX.Element | null
```

## With rich text (`nodesToJSX`)

```tsx
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)

Uploads come back as either the full document or just the ID, depending on depth — always check.

```tsx
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 references

```tsx
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 fields

```tsx
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 inheritance

Theme `'inherit'` means use the parent's theme — pass `undefined` to skip applying a theme on this block.

```tsx
<Section
  theme={theme && theme !== 'inherit' ? theme : undefined}
  layoutBackground={
    theme && theme !== 'inherit' ? layoutBg : undefined
  }
/>
```

## Block name → ID/ARIA

```tsx
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}
/>
```

> [!NOTE]
> **Interactive blocks don't borrow `blockName` for their accessible name.** The built-in **card** block, when it resolves to a link (`<Card.a>`), takes its accessible name from the visible card content — it does **not** inject an `aria-label` from `blockName` (that would override the visible text). `blockName` is still used for the anchor `id`, and a card link that opens a new tab appends a visually hidden `(opens in new tab)` hint for screen readers.

## Inline blocks (button, icon)

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:

```tsx
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](https://docs.systhema.app/sk/payload/editor/icons.md#custom-inline-block-converters).
