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.

Lexical Overview

One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.

Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.

Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:

  1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
  2. A "hover" toolbar that pops up if you select text
  3. It supports Payload blocks natively, directly within your rich text editor
  4. Custom elements, called "features", are much easier to build in Lexical vs. Slate

To use the Lexical editor, first you need to install it:

1
npm install @payloadcms/richtext-lexical

Once you have it installed, you can pass it to your top-level Payload Config as follows:

1
import { buildConfig } from 'payload'
2
import { lexicalEditor } from '@payloadcms/richtext-lexical'
3
4
export default buildConfig({
5
collections: [
6
// your collections here
7
],
8
// Pass the Lexical editor to the root config
9
editor: lexicalEditor({}),
10
})

You can also override Lexical settings on a field-by-field basis as follows:

1
import type { CollectionConfig } from 'payload'
2
import { lexicalEditor } from '@payloadcms/richtext-lexical'
3
4
export const Pages: CollectionConfig = {
5
slug: 'pages',
6
fields: [
7
{
8
name: 'content',
9
type: 'richText',
10
// Pass the Lexical editor here and override base settings as necessary
11
editor: lexicalEditor({}),
12
},
13
],
14
}

Extending the lexical editor with Features

Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.

Features: The Building Blocks

At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.

If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.

Integrating New Features

To weave in your custom features, utilize the features prop when initializing the Lexical Editor. Here's a basic example of how this is done:

1
import {
2
BlocksFeature,
3
LinkFeature,
4
UploadFeature,
5
lexicalEditor,
6
} from '@payloadcms/richtext-lexical'
7
import { Banner } from '../blocks/Banner'
8
import { CallToAction } from '../blocks/CallToAction'
9
10
{
11
editor: lexicalEditor({
12
features: ({ defaultFeatures, rootFeatures }) => [
13
...defaultFeatures,
14
LinkFeature({
15
// Example showing how to customize the built-in fields
16
// of the Link feature
17
fields: ({ defaultFields }) => [
18
...defaultFields,
19
{
20
name: 'rel',
21
label: 'Rel Attribute',
22
type: 'select',
23
hasMany: true,
24
options: ['noopener', 'noreferrer', 'nofollow'],
25
admin: {
26
description:
27
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
28
},
29
},
30
],
31
}),
32
UploadFeature({
33
collections: {
34
uploads: {
35
// Example showing how to customize the built-in fields
36
// of the Upload feature
37
fields: [
38
{
39
name: 'caption',
40
type: 'richText',
41
editor: lexicalEditor(),
42
},
43
],
44
},
45
},
46
}),
47
// This is incredibly powerful. You can re-use your Payload blocks
48
// directly in the Lexical editor as follows:
49
BlocksFeature({
50
blocks: [Banner, CallToAction],
51
}),
52
],
53
})
54
}

features can be both an array of features, or a function returning an array of features. The function provides the following props:

PropDescription
defaultFeaturesThis opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below.
rootFeaturesThis array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty.

Features overview

Here's an overview of all the included features:

Feature NameIncluded by defaultDescription
BoldTextFeatureYesHandles the bold text format
ItalicTextFeatureYesHandles the italic text format
UnderlineTextFeatureYesHandles the underline text format
StrikethroughTextFeatureYesHandles the strikethrough text format
SubscriptTextFeatureYesHandles the subscript text format
SuperscriptTextFeatureYesHandles the superscript text format
InlineCodeTextFeatureYesHandles the inline-code text format
ParagraphFeatureYesHandles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs
HeadingFeatureYesAdds Heading Nodes (by default, H1 - H6, but that can be customized)
AlignFeatureYesAllows you to align text left, centered and right
IndentFeatureYesAllows you to indent text with the tab key
UnorderedListFeatureYesAdds unordered lists (ul)
OrderedListFeatureYesAdds ordered lists (ol)
CheckListFeatureYesAdds checklists
LinkFeatureYesAllows you to create internal and external links
RelationshipFeatureYesAllows you to create block-level (not inline) relationships to other documents
BlockQuoteFeatureYesAllows you to create block-level quotes
UploadFeatureYesAllows you to create block-level upload nodes - this supports all kinds of uploads, not just images
HorizontalRuleFeatureYesHorizontal rules / separators. Basically displays an <hr> element
InlineToolbarFeatureYesThe inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text
FixedToolbarFeatureNoThis classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time.
BlocksFeatureNoAllows you to use Payload's Blocks Field directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field.
TreeViewFeatureNoAdds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging
EXPERIMENTAL_TableFeatureNoAdds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release.

Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!

Creating your own, custom Feature

You can find more information about creating your own feature in our building custom feature docs.

TypeScript

Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from @payloadcms/richtext-lexical - each type is prefixed with Serialized, e.g. SerializedUploadNode.

In order to fully type the entire editor JSON, you can use our TypedEditorState helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:

1
import type {
2
SerializedAutoLinkNode,
3
SerializedBlockNode,
4
SerializedHorizontalRuleNode,
5
SerializedLinkNode,
6
SerializedListItemNode,
7
SerializedListNode,
8
SerializedParagraphNode,
9
SerializedQuoteNode,
10
SerializedRelationshipNode,
11
SerializedTextNode,
12
SerializedUploadNode,
13
TypedEditorState,
14
SerializedHeadingNode,
15
} from '@payloadcms/richtext-lexical'
16
17
const editorState: TypedEditorState<
18
| SerializedAutoLinkNode
19
| SerializedBlockNode
20
| SerializedHorizontalRuleNode
21
| SerializedLinkNode
22
| SerializedListItemNode
23
| SerializedListNode
24
| SerializedParagraphNode
25
| SerializedQuoteNode
26
| SerializedRelationshipNode
27
| SerializedTextNode
28
| SerializedUploadNode
29
| SerializedHeadingNode
30
> = {
31
root: {
32
type: 'root',
33
direction: 'ltr',
34
format: '',
35
indent: 0,
36
version: 1,
37
children: [
38
{
39
children: [
40
{
41
detail: 0,
42
format: 0,
43
mode: 'normal',
44
style: '',
45
text: 'Some text. Every property here is fully-typed',
46
type: 'text',
47
version: 1,
48
},
49
],
50
direction: 'ltr',
51
format: '',
52
indent: 0,
53
type: 'paragraph',
54
textFormat: 0,
55
version: 1,
56
},
57
],
58
},
59
}

Alternatively, you can use the DefaultTypedEditorState type, which includes all types for all nodes included in the defaultFeatures:

1
import type {
2
DefaultTypedEditorState
3
} from '@payloadcms/richtext-lexical'
4
5
const editorState: DefaultTypedEditorState = {
6
root: {
7
type: 'root',
8
direction: 'ltr',
9
format: '',
10
indent: 0,
11
version: 1,
12
children: [
13
{
14
children: [
15
{
16
detail: 0,
17
format: 0,
18
mode: 'normal',
19
style: '',
20
text: 'Some text. Every property here is fully-typed',
21
type: 'text',
22
version: 1,
23
},
24
],
25
direction: 'ltr',
26
format: '',
27
indent: 0,
28
type: 'paragraph',
29
textFormat: 0,
30
version: 1,
31
},
32
],
33
},
34
}

Just like TypedEditorState, the DefaultTypedEditorState also accepts an optional node type union as a generic. Here, this would add the specified node types to the default ones. Example: DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>.

This is a type-safe representation of the editor state. Looking at the auto-suggestions of type it will show you all the possible node types you can use.

Make sure to only use types exported from @payloadcms/richtext-lexical, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.

Automatic type generation

Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.

Next

Lexical Converters