You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(richtext-lexical): add TextStateFeature (allows applying styles such as color and background color to text) (#9667)
Originally this PR was going to introduce a `TextColorFeature`, but it
ended up becoming a more general-purpose `TextStateFeature`.
## Example of use:
```ts
import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical'
TextStateFeature({
// prettier-ignore
state: {
color: {
...defaultColors,
// fancy gradients!
galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
},
// You can have both colored and underlined text at the same time.
// If you don't want that, you should group them within the same key.
// (just like I did with defaultColors and my fancy gradients)
underline: {
'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
// You'll probably want to use the CSS light-dark() utility.
'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
},
},
}),
```
Which will result in the following:

## Challenges & Considerations
Adding colors or styles in general to the Lexical editor is not as
simple as it seems.
1. **Extending TextNode isn't ideal**
- While possible, it's verbose, error-prone, and not composable. If
multiple features extend the same node, conflicts arise.
- That’s why we collaborated with the Lexical team to introduce [the new
State API](https://lexical.dev/docs/concepts/node-replacement)
([PR](facebook/lexical#7117)).
2. **Issues with patchStyles**
- Some community plugins use `patchStyles`, but storing CSS in the
editor’s JSON has drawbacks:
- Style adaptability: Users may want different styles per scenario
(dark/light mode, mobile/web, etc.).
- Migration challenges: Hardcoded colors (e.g., #FF0000) make updates
difficult. Using tokens (e.g., "red") allows flexibility.
- Larger JSON footprint increases DB size.
3. **Managing overlapping styles**
- Some users may want both text and background colors on the same node,
while others may prefer mutual exclusivity.
- This approach allows either:
- Using a single "color" state (e.g., "bg-red" + "text-red").
- Defining separate "bg-color" and "text-color" states for independent
styling.
4. **Good light and dark modes by default**
- Many major editors (Google Docs, OneNote, Word) treat dark mode as an
afterthought, leading to poor UX.
- We provide a well-balanced default palette that looks great in both
themes, serving as a strong foundation for customization.
5. **Feature name. Why TextState?**
- Other names considered were `TextFormatFeature` and
`TextStylesFeature`. The term `format` in Lexical and Payload is already
used to refer to something else (italic, bold, etc.). The term `style`
could be misleading since it is never attached to the editorState.
- State seems appropriate because:
- Lexical's new state API is used under the hood.
- Perhaps in the future we'll want to make state features for other
nodes, such as `ElementStateFeature` or `RootStateFeature`.
Note: There's a bug in Lexical's `forEachSelectedTextNode`. When the
selection includes a textNode partially on the left, all state for that
node is removed instead of splitting it along the selection edge.
|**`BoldFeature`**| Yes | Handles the bold text format |
148
-
|**`ItalicFeature`**| Yes | Handles the italic text format |
149
-
|**`UnderlineFeature`**| Yes | Handles the underline text format |
150
-
|**`StrikethroughFeature`**| Yes | Handles the strikethrough text format |
151
-
|**`SubscriptFeature`**| Yes | Handles the subscript text format |
152
-
|**`SuperscriptFeature`**| Yes | Handles the superscript text format |
153
-
|**`InlineCodeFeature`**| Yes | Handles the inline-code text format |
154
-
|**`ParagraphFeature`**| Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
155
-
|**`HeadingFeature`**| Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
156
-
|**`AlignFeature`**| Yes | Allows you to align text left, centered and right |
157
-
|**`IndentFeature`**| Yes | Allows you to indent text with the tab key |
|**`LinkFeature`**| Yes | Allows you to create internal and external links |
162
-
|**`RelationshipFeature`**| Yes | Allows you to create block-level (not inline) relationships to other documents |
163
-
|**`BlockquoteFeature`**| Yes | Allows you to create block-level quotes |
164
-
|**`UploadFeature`**| Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
165
-
|**`HorizontalRuleFeature`**| Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
166
-
|**`InlineToolbarFeature`**| Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
167
-
|**`FixedToolbarFeature`**| No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
168
-
|**`BlocksFeature`**| No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
169
-
|**`TreeViewFeature`**| No | Adds 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 |
170
-
|**`EXPERIMENTAL_TableFeature`**| No | Adds 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. |
145
+
| Feature Name | Included by default | Description |
|**`BoldFeature`**| Yes | Handles the bold text format |
148
+
|**`ItalicFeature`**| Yes | Handles the italic text format |
149
+
|**`UnderlineFeature`**| Yes | Handles the underline text format |
150
+
|**`StrikethroughFeature`**| Yes | Handles the strikethrough text format |
151
+
|**`SubscriptFeature`**| Yes | Handles the subscript text format |
152
+
|**`SuperscriptFeature`**| Yes | Handles the superscript text format |
153
+
|**`InlineCodeFeature`**| Yes | Handles the inline-code text format |
154
+
|**`ParagraphFeature`**| Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
155
+
|**`HeadingFeature`**| Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
156
+
|**`AlignFeature`**| Yes | Allows you to align text left, centered and right |
157
+
|**`IndentFeature`**| Yes | Allows you to indent text with the tab key |
|**`LinkFeature`**| Yes | Allows you to create internal and external links |
162
+
|**`RelationshipFeature`**| Yes | Allows you to create block-level (not inline) relationships to other documents |
163
+
|**`BlockquoteFeature`**| Yes | Allows you to create block-level quotes |
164
+
|**`UploadFeature`**| Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
165
+
|**`HorizontalRuleFeature`**| Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
166
+
|**`InlineToolbarFeature`**| Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
167
+
|**`FixedToolbarFeature`**| No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
168
+
|**`BlocksFeature`**| No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
169
+
|**`TreeViewFeature`**| No | Adds 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 |
170
+
|**`EXPERIMENTAL_TableFeature`**| No | Adds 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. |
171
+
|**`EXPERIMENTAL_TextStateFeature`**| No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
171
172
172
173
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!
0 commit comments