Skip to main content
Before building a custom feature, become familiar with the Lexical concepts, especially nodes, editor state, selections, and commands. A feature has a server entry point and, when it adds interactive editor behavior, a separate client entry point. By convention, place them in feature.server.ts and feature.client.ts. Applications import only the server feature. It registers the client feature through a component path.
Import Lexical APIs through @frogbotai/richtext-lexical and its /lexical/* subpaths. Direct imports from Lexical packages can break when FrogBot updates its supported Lexical version.

Do you need a custom feature?

Use Blocks when editors only need to insert a structured block or inline block. Blocks support fields, custom editor components, and converters without requiring a custom node or plugin. Build a custom feature when you need lower-level editor behavior, such as a new node type, command, toolbar control, listener, transform, or context provider. This guide builds a DividerFeature that inserts a decorated divider from a command, toolbar item, slash-menu item, or the +++ Markdown shortcut.

Server feature

The server feature is the public entry point:
createServerFeature returns a function. Call it, as in DividerFeature(), to pass the feature’s props and get the provider you add to features.

Server feature properties

The feature object, or the value the feature function returns, accepts these properties: Add the feature to a rich text editor:

Internationalization

Feature translations are scoped to the feature key. This label is available as lexical:divider:label:

Markdown transformers

Server Markdown transformers are used by the Markdown converters. The following transformer exports a divider as +++ and imports a line containing three plus signs:
Register it on the server feature:

Nodes

Register nodes on the server for headless editing, conversion, hooks, validation, and nested fields. createNode preserves the node’s serialized type across these options:
The component path is resolved from the import-map base directory. Include the named export after #. FrogBot does not expose or mount a public GraphQL execution runtime by default. graphQLPopulationPromises is conditional integration metadata only: it has no effect unless your deployment provides and verifies a compatible public GraphQL runtime. It is not needed for FrogBot’s public REST runtime.

Feature load order

The feature property can be a function. It receives all configured feature providers and the features that have already resolved:
featureProviderMap contains every configured feature, including features that have not loaded. Use resolvedFeatures when you need the output of an earlier feature. The dependency options on createServerFeature are listed in the options table.
The editor dependency installed with this release (3.90.2) reverses dependency traversal order before checking priority dependencies. Declaring dependenciesPriority: ['paragraph'] fails even when the paragraph feature is configured, regardless of its position in the feature array. Soft dependencies do not provide a reliable load-first guarantee either.
When you only need a feature to be present, check featureProviderMap. This example works in either feature-array order and does not require the paragraph feature to have resolved:

Client feature

The client feature owns interactive behavior such as rendered nodes, plugins, toolbar controls, slash-menu items, and editor-side Markdown shortcuts. Keep it in a separate client module:
Do not import runtime values from the package root in a client module. Use @frogbotai/richtext-lexical/client and /lexical/* subpaths. Type-only root imports are safe. createClientFeature accepts an object with these properties, or a function that returns one. The function form is described in Feature props. Register the client feature from feature.server.ts:

Nodes

A custom node used in the editor must be registered by both server and client features. The same node class can be imported by either side, so do not add a 'use client' directive to the node module. divider/nodes/DividerNode.tsx:
divider/components/DividerComponent.tsx:
Register the node on the client:

Plugins

Plugins are React components mounted inside the editor’s Lexical providers. They commonly register commands, listeners, or node transforms. divider/plugin.tsx registers a command that inserts the custom node:
Plugins are registered as entries with a component and position:

Toolbar groups

Toolbar groups are either horizontal icon buttons or dropdowns containing labeled items. Groups with the same key are merged.
The package also exports standard groups from /client: toolbarAddDropdownGroupWithItems, toolbarTextDropdownGroupWithItems, toolbarFormatGroupWithItems, and toolbarFeatureButtonsGroupWithItems.

Toolbar items

Use the same item in toolbarFixed and toolbarInline when it should appear in both toolbars:
divider/components/DividerIcon.tsx:

Slash-menu groups

Slash-menu groups with the same key are merged:
Use the exported slashMenuBasicGroupWithItems helper to add entries to the standard Basic group.

Slash-menu items

Markdown transformers

Client Markdown transformers create nodes as an editor types. Use the same transformer and node as the server feature so interactive shortcuts and explicit conversions agree:
Typing +++ followed by a space inserts a DividerNode. The escaped expression /^\+\+\+\s*$/ matches literal plus signs; unescaped plus signs are invalid repetition operators.

Providers

Providers supply React context to the feature’s nodes and plugins. Each provider receives the nested editor content as children:

Feature props

The three server generics represent user input, sanitized server props, and serializable client props. The two client generics represent incoming client props and sanitized client props. Use a feature function to sanitize input and return the result as sanitizedServerFeatureProps:
Client sanitization follows the same pattern:
The client feature function receives:

Pass props from server to client

Server props are not sent to the client automatically. Return the client subset through clientFeatureProps, as in the server example above. Values crossing this boundary must be serializable. Keep functions, class instances, maps, and server-only values in the server feature.