Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Blocks

The BlocksFeature allows you to embed Payload's Blocks Field directly inside your Lexical rich text editor. This provides a powerful way to create structured, reusable content components within your rich text content.

Basic Setup

lexical blocks example

To add blocks to your Lexical editor, include the BlocksFeature in your editor configuration:

1
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'
2
3
{
4
name: 'content',
5
type: 'richText',
6
editor: lexicalEditor({
7
features: ({ defaultFeatures }) => [
8
...defaultFeatures,
9
BlocksFeature({
10
blocks: [
11
{
12
slug: 'banner',
13
fields: [
14
{
15
name: 'style',
16
type: 'select',
17
options: ['info', 'warning', 'error', 'success'],
18
defaultValue: 'info',
19
},
20
{
21
name: 'content',
22
type: 'textarea',
23
required: true,
24
},
25
],
26
},
27
{
28
slug: 'cta',
29
fields: [
30
{
31
name: 'heading',
32
type: 'text',
33
required: true,
34
},
35
{
36
name: 'link',
37
type: 'text',
38
},
39
],
40
},
41
],
42
}),
43
],
44
}),
45
}

Blocks use the same configuration schema as Blocks within Payload's Blocks Field.

Blocks vs Inline Blocks

Lexical blocks and inline blocks example

The BlocksFeature supports two types of blocks:

Blocks

Regular blocks are block-level elements that take up an entire line, similar to paragraphs or headings. They cannot be placed inline with text.

Use blocks for:

  • Call-to-action sections
  • Image galleries
  • Code snippets
  • Embedded content (videos, maps)
  • Any component that should stand alone

Inline Blocks

Inline blocks can be inserted within text, appearing alongside other content in the same paragraph. They're useful for elements that need to flow with text.

Use inline blocks for:

  • Mentions (@user)
  • Custom badges or tags
  • Inline icons or emojis
  • Variable placeholders
  • Footnote references
1
BlocksFeature({
2
// Block-level blocks
3
blocks: [
4
{
5
slug: 'callout',
6
fields: [{ name: 'content', type: 'textarea' }],
7
},
8
],
9
// Inline blocks (appear within text)
10
inlineBlocks: [
11
{
12
slug: 'mention',
13
fields: [
14
{
15
name: 'user',
16
type: 'relationship',
17
relationTo: 'users',
18
required: true,
19
},
20
],
21
},
22
],
23
})

Data Structure

Block data is stored within the Lexical JSON structure. Each block node contains a fields object with all the block's field values:

1
{
2
"type": "block",
3
"version": 2,
4
"fields": {
5
"id": "65298b13db4ef8c744a7faaa", // default field (required, auto-generated)
6
"blockType": "banner", // default field (required, identifies the block)
7
"blockName": "Important Notice", // default field (optional, custom label for the block instance)
8
"style": "warning", // custom field
9
"content": "This is the block content..." // custom field
10
}
11
}

Inline blocks follow a similar structure with type: "inlineBlock".

Custom Block Components

You can customize how blocks appear in the editor by providing custom React components. This is useful when you want a more visual representation of your block content.

Block Components

Lexical Block with custom Block component

For regular blocks, use the admin.components.Block property to provide a custom component:

1
{
2
slug: 'myCustomBlock',
3
admin: {
4
components: {
5
Block: '/path/to/MyBlockComponent#MyBlockComponent',
6
},
7
},
8
fields: [
9
{
10
name: 'style',
11
type: 'select',
12
options: ['primary', 'secondary'],
13
},
14
],
15
}

Your custom component can use composable primitives from @payloadcms/richtext-lexical/client. These components automatically receive block data from context, so you can use them to recreate the default block UI or arrange them in custom layouts:

1
'use client'
2
import type { LexicalBlockClientProps } from '@payloadcms/richtext-lexical'
3
4
import {
5
BlockCollapsible,
6
BlockEditButton,
7
BlockRemoveButton,
8
} from '@payloadcms/richtext-lexical/client'
9
import { useFormFields } from '@payloadcms/ui'
10
11
export const MyBlockComponent: React.FC<LexicalBlockClientProps> = () => {
12
const style = useFormFields(([fields]) => fields.style)
13
14
return (
15
<BlockCollapsible removeButton={false}>
16
<div>Style: {(style?.value as string) ?? 'none'}</div>
17
<div>
18
You can manually render the remove and edit buttons if you want to:
19
</div>
20
<div style={{ display: 'flex' }}>
21
<BlockEditButton />
22
<BlockRemoveButton />
23
</div>
24
</BlockCollapsible>
25
)
26
}

The BlockCollapsible component automatically renders an edit button that opens a drawer with the block's fields. You can customize this behavior by passing props like removeButton={false} to hide the default remove button and render it yourself.

You can also choose to render something completely different in your custom block component:

Lexical Block with custom Block component, not reusing the original block components
1
'use client'
2
import type { LexicalBlockClientProps } from '@payloadcms/richtext-lexical'
3
4
import {
5
BlockEditButton,
6
BlockRemoveButton,
7
} from '@payloadcms/richtext-lexical/client'
8
import { useFormFields } from '@payloadcms/ui'
9
10
export const BlockComponent: React.FC<LexicalBlockClientProps> = () => {
11
const content = useFormFields(([fields]) => fields.content)
12
13
return (
14
<div
15
style={{
16
background: '#6198FF',
17
borderRadius: 8,
18
color: 'black',
19
padding: 16,
20
}}
21
>
22
<div
23
style={{
24
display: 'flex',
25
justifyContent: 'space-between',
26
marginBottom: 8,
27
}}
28
>
29
<strong>⚠️ Banner</strong>
30
<div style={{ display: 'flex' }}>
31
<BlockEditButton />
32
<BlockRemoveButton />
33
</div>
34
</div>
35
<p style={{ margin: 0 }}>{(content?.value as string) || 'No content'}</p>
36
</div>
37
)
38
}

Inline Block Components

For inline blocks, similar composable primitives are available:

Lexical Block with custom inline Block component
1
'use client'
2
import type { LexicalInlineBlockClientProps } from '@payloadcms/richtext-lexical'
3
4
import {
5
InlineBlockContainer,
6
InlineBlockEditButton,
7
InlineBlockLabel,
8
InlineBlockRemoveButton,
9
} from '@payloadcms/richtext-lexical/client'
10
11
export const MyInlineBlockComponent: React.FC<
12
LexicalInlineBlockClientProps
13
> = () => {
14
return (
15
<InlineBlockContainer>
16
<span style={{ backgroundColor: 'lightgreen', color: 'black' }}>1</span>
17
<InlineBlockLabel />
18
<span style={{ backgroundColor: 'lightgreen', color: 'black' }}>2</span>
19
<InlineBlockEditButton />
20
<span style={{ backgroundColor: 'lightgreen', color: 'black' }}>3</span>
21
<InlineBlockRemoveButton />
22
</InlineBlockContainer>
23
)
24
}

Or, you can choose to render something completely different in your custom inline block component, for example a badge with a username:

Lexical Block with custom inline Block component, username tag example
1
'use client'
2
import type { LexicalInlineBlockClientProps } from '@payloadcms/richtext-lexical'
3
4
import {
5
InlineBlockEditButton,
6
InlineBlockRemoveButton,
7
} from '@payloadcms/richtext-lexical/client'
8
import { useFormFields } from '@payloadcms/ui'
9
10
export const MyInlineBlockComponent: React.FC<
11
LexicalInlineBlockClientProps
12
> = () => {
13
const username = useFormFields(([fields]) => fields.username)
14
15
return (
16
<div
17
style={{
18
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
19
borderRadius: 12,
20
color: 'white',
21
display: 'flex',
22
fontFamily: 'var(--font-body)',
23
fontSize: 13,
24
padding: '2px 8px',
25
}}
26
>
27
@{(username?.value as string) || 'username'}
28
<div style={{ color: 'white', fill: 'inline-flex' }}>
29
<InlineBlockEditButton />
30
<InlineBlockRemoveButton />
31
</div>
32
</div>
33
)
34
}

Label Components

You can also customize the label shown in the block header using admin.components.Label. This is useful for displaying dynamic information based on the block's field values.

Block Label:

Block in Lexical field with custom label component
1
'use client'
2
import type { LexicalBlockLabelClientProps } from '@payloadcms/richtext-lexical'
3
4
import { useFormFields } from '@payloadcms/ui'
5
6
export const MyBlockLabel: React.FC<LexicalBlockLabelClientProps> = () => {
7
const title = useFormFields(([fields]) => fields.title)
8
9
return (
10
<div style={{ backgroundColor: 'lightgreen', color: 'black' }}>
11
Custom Label. Value of title field: {title?.value as string}
12
</div>
13
)
14
}

Inline Block Label:

Inline Block in Lexical field with custom label component
1
'use client'
2
import type { LexicalInlineBlockLabelClientProps } from '@payloadcms/richtext-lexical'
3
4
import { useFormFields } from '@payloadcms/ui'
5
6
export const MyInlineBlockLabel: React.FC<
7
LexicalInlineBlockLabelClientProps
8
> = () => {
9
const name = useFormFields(([fields]) => fields.name)
10
11
return (
12
<span style={{ backgroundColor: 'lightgreen', color: 'black' }}>
13
Custom Label. Name field: {name?.value as string}
14
</span>
15
)
16
}

Example: Pre-made CodeBlock

For a real-world example of a custom block component, see the source code for Payload's pre-made CodeBlock. It's a standard block with a custom admin.components.Block component that uses the same APIs documented above—including useFormFields, BlockCollapsible, and the helper buttons.

TypeScript

When building custom block components, you can import the following types for proper typing:

1
import type {
2
// Block component types
3
LexicalBlockClientProps,
4
LexicalBlockServerProps,
5
6
// Block label component types
7
LexicalBlockLabelClientProps,
8
LexicalBlockLabelServerProps,
9
10
// Inline block component types
11
LexicalInlineBlockClientProps,
12
LexicalInlineBlockServerProps,
13
14
// Inline block label component types
15
LexicalInlineBlockLabelClientProps,
16
LexicalInlineBlockLabelServerProps,
17
} from '@payloadcms/richtext-lexical'

Type

Use Case

LexicalBlockClientProps

Client component for admin.components.Block

LexicalBlockServerProps

Server component for admin.components.Block

LexicalBlockLabelClientProps

Client component for admin.components.Label

LexicalBlockLabelServerProps

Server component for admin.components.Label

LexicalInlineBlockClientProps

Client component for inline admin.components.Block

LexicalInlineBlockServerProps

Server component for inline admin.components.Block

LexicalInlineBlockLabelClientProps

Client component for inline admin.components.Label

LexicalInlineBlockLabelServerProps

Server component for inline admin.components.Label

Rendering Blocks

When rendering rich text content on the frontend, blocks need to be handled by your converter configuration. See the following guides for details:

Each converter allows you to define custom renderers for your block types, giving you full control over how block content appears on your frontend.

Code Block

Code Block in Lexical field

Payload provides a pre-built CodeBlock that you can use directly in your projects. It includes syntax highlighting, language selection, and optional TypeScript type definitions support:

1
import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical'
2
3
BlocksFeature({
4
blocks: [
5
CodeBlock({
6
defaultLanguage: 'ts',
7
languages: {
8
plaintext: 'Plain Text',
9
js: 'JavaScript',
10
ts: 'TypeScript',
11
tsx: 'TSX',
12
jsx: 'JSX',
13
},
14
}),
15
],
16
})

CodeBlock Options

Option

Description

slug

Override the block slug. Default: 'Code'

defaultLanguage

The default language selection. Default: first key in languages

languages

Object mapping language keys to display labels

typescript

TypeScript-specific configuration (see below)

fieldOverrides

Partial block config to override or extend the default CodeBlock

TypeScript Support

When using TypeScript as a language option, you can load external type definitions to provide IntelliSense in the editor:

1
CodeBlock({
2
slug: 'PayloadCode',
3
languages: {
4
ts: 'TypeScript',
5
},
6
typescript: {
7
fetchTypes: [
8
{
9
// In the url you can use @latest or a specific version (e.g. @3.68.5)
10
url: 'https://unpkg.com/payload@latest/dist/index.bundled.d.ts',
11
filePath: 'file:///node_modules/payload/index.d.ts',
12
},
13
{
14
url: 'https://unpkg.com/@types/react@latest/index.d.ts',
15
filePath: 'file:///node_modules/@types/react/index.d.ts',
16
},
17
],
18
paths: {
19
payload: ['file:///node_modules/payload/index.d.ts'],
20
react: ['file:///node_modules/@types/react/index.d.ts'],
21
},
22
typeRoots: ['node_modules/@types', 'node_modules/payload'],
23
enableSemanticValidation: true,
24
},
25
})

TypeScript Option

Description

fetchTypes

Array of { url, filePath } objects to fetch external type definitions

paths

Module path mappings for import resolution

typeRoots

Directories to search for type definitions. Default: ['node_modules/@types']

target

TypeScript compilation target. Default: 'ESNext'

enableSemanticValidation

Enable full type checking (not just syntax). Default: false

Was this page helpful?

Next

Custom Features