> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Features

> Build custom server and client features for FrogBot's Lexical editor.

Before building a custom feature, become familiar with the [Lexical concepts](https://lexical.dev/docs/concepts/editor-state), 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](https://docs.frogbot.ai/custom-components/overview.md#component-paths).

> [!WARNING]
>
> 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](https://docs.frogbot.ai/rich-text/blocks.md) 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:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

export const DividerFeature = createServerFeature({
  feature: {},
  key: 'divider',
})
```

`createServerFeature` returns a function. Call it, as in `DividerFeature()`, to pass the feature's props and get the provider you add to `features`.

| Option | Type | Default | Description |
| - | - | - | - |
| `key` | `string` | Required | Unique feature key. Other features refer to it in dependencies and through `featureProviderMap`, and translations are scoped to it. |
| `feature` | `ServerFeature \| (args) => ServerFeature \| Promise<ServerFeature>` | Required | The feature definition described in [Server feature properties](#server-feature-properties). A function receives the arguments in [Feature load order](#feature-load-order) and may be async. The object form cannot set `sanitizedServerFeatureProps`. |
| `dependencies` | `string[]` | `[]` | Feature keys that must be configured in the same editor. They do not have to load first. A missing one throws when the config loads. |
| `dependenciesPriority` | `string[]` | `[]` | Feature keys that must be configured and load first, so their output is in `resolvedFeatures`. See the warning in [Feature load order](#feature-load-order). |
| `dependenciesSoft` | `string[]` | `[]` | Feature keys to load first when they are configured. Missing ones are ignored. See the warning in [Feature load order](#feature-load-order). |

### Server feature properties

The `feature` object, or the value the `feature` function returns, accepts these properties:

| Option | Type | Default | Description |
| - | - | - | - |
| `ClientFeature` | `string \| { path: string; exportName?: string }` | None | [Component path](https://docs.frogbot.ai/custom-components/overview.md#component-paths) to the [client feature](#client-feature). Omit it when the feature has no editor behavior. |
| `clientFeatureProps` | `ClientProps` | None | Serializable props sent to the client feature. See [Pass props from server to client](#pass-props-from-server-to-client). |
| `sanitizedServerFeatureProps` | `ServerProps` | None | The feature's sanitized server props, stored on the resolved feature for other server code to read. Only the function form of `feature` can set it. See [Feature props](#feature-props). |
| `i18n` | `Partial<Record<language, Record<string, string>>>` | None | Translations keyed by language code. Each key is available as `lexical:<featureKey>:<key>`. See [Internationalization](#internationalization). |
| `nodes` | `NodeWithHooks[]` | `[]` | Nodes registered on the server, with their converters, hooks, and validation. See [Nodes](#server-feature-nodes). |
| `markdownTransformers` | `(Transformer \| ({ allNodes, allTransformers }) => Transformer)[]` | `[]` | Transformers used by the [Markdown converters](https://docs.frogbot.ai/rich-text/converting-markdown.md). A function receives every registered node and the transformers collected so far. See [Markdown transformers](#server-feature-markdown-transformers). |
| `hooks` | `{ afterChange?, afterRead?, beforeChange?, beforeValidate? }` | None | Arrays of hooks run for the whole rich text field value, in addition to any node hooks. Each hook receives the field hook arguments and returns the field value to use. Every hook receives the original arguments, so when several features set the same hook, the value from the last one is used. |
| `generateSchemaMap` | `({ config, field, i18n, props, schemaMap, schemaPath }) => FieldSchemaMap \| null` | None | Registers the feature's nested fields, keyed by path, so the Admin Panel can build form state for drawers that edit them. Return `null` when there are none. |
| `componentImports` | `Record<string, ComponentPath> \| ComponentPath[] \| (args) => void` | None | Components added to the [import map](https://docs.frogbot.ai/custom-components/overview.md#import-map). With the object form, each component is also passed to client features in `featureClientImportMap` as `<featureKey>.<key>`. |

Add the feature to a rich text editor:

```ts theme={"theme":"tokyo-night"}
import { lexicalEditor } from '@frogbotai/richtext-lexical'
import type { CollectionConfig } from 'frogbot'

import { DividerFeature } from './divider/feature.server'

export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [
          ...defaultFeatures,
          DividerFeature(),
        ],
      }),
    },
  ],
}
```

### Internationalization

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

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

export const DividerFeature = createServerFeature({
  feature: {
    i18n: {
      de: { label: 'Trennlinie' },
      en: { label: 'Divider' },
    },
  },
  key: 'divider',
})
```

### Markdown transformers

Server Markdown transformers are used by the [Markdown converters](https://docs.frogbot.ai/rich-text/converting-markdown.md). The following transformer exports a divider as `+++` and imports a line containing three plus signs:

```ts theme={"theme":"tokyo-night"}
import type { ElementTransformer } from '@frogbotai/richtext-lexical/lexical/markdown'

import { $createDividerNode, $isDividerNode, DividerNode } from './nodes/DividerNode'

export const DividerMarkdownTransformer: ElementTransformer = {
  dependencies: [DividerNode],
  export: (node) => ($isDividerNode(node) ? '+++' : null),
  regExp: /^\+\+\+\s*$/,
  replace: (parentNode) => {
    parentNode.replace($createDividerNode())
  },
  type: 'element',
}
```

Register it on the server feature:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

import { DividerMarkdownTransformer } from './markdownTransformer'

export const DividerFeature = createServerFeature({
  feature: {
    markdownTransformers: [DividerMarkdownTransformer],
  },
  key: 'divider',
})
```

### 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:

```ts theme={"theme":"tokyo-night"}
import { createNode, createServerFeature } from '@frogbotai/richtext-lexical'
import type { HTMLConverters } from '@frogbotai/richtext-lexical/html'
import type { Field } from 'frogbot'

import { DividerMarkdownTransformer } from './markdownTransformer'
import {
  DividerNode,
  type SerializedDividerNode,
} from './nodes/DividerNode'

type DividerFeatureInput = {
  enableGraphQLPopulation?: boolean
}

const dividerFields: Field[] = [
  {
    name: 'title',
    type: 'text',
    required: true,
  },
]

// Pass to the HTML converters' `converters` option to render dividers.
export const dividerHTMLConverters: HTMLConverters<
  SerializedDividerNode & { type: 'divider' }
> = {
  divider: '<hr>',
}

export const DividerFeature = createServerFeature<DividerFeatureInput>({
  feature: ({ props }) => ({
    ClientFeature: './divider/feature.client#DividerClientFeature',
    i18n: {
      de: { label: 'Trennlinie' },
      en: { label: 'Divider' },
    },
    markdownTransformers: [DividerMarkdownTransformer],
    nodes: [
      createNode<DividerNode, SerializedDividerNode>({
        getSubFields: () => dividerFields,
        getSubFieldsData: ({ node }) => node.fields,
        ...(props?.enableGraphQLPopulation
          ? {
              graphQLPopulationPromises: [
                ({ node, populationPromises }) => {
                  populationPromises.push(
                    Promise.resolve().then(() => {
                      node.fields.title = node.fields.title.trim()
                    }),
                  )
                },
              ],
            }
          : {}),
        hooks: {
          beforeValidate: [
            ({ node }) => ({
              ...node,
              fields: {
                ...node.fields,
                title: node.fields.title.trim(),
              },
            }),
          ],
        },
        node: DividerNode,
        validations: [
          ({ node }) =>
            node.fields.title.length > 0 || 'Divider title must not be empty',
        ],
      }),
    ],
  }),
  key: 'divider',
})
```

The component path is resolved from the import-map base directory. Include the named export after `#`.

| Option | Type | Default | Description |
| - | - | - | - |
| `node` | `Klass<LexicalNode> \| LexicalNodeReplacement` | Required | Registers the node class or a [node replacement](https://lexical.dev/docs/concepts/node-replacement). |
| `jsonSchema` | `(args) => JSONSchema4` | None | Returns the node's JSON Schema, so generated types list it in the editor's node union. Nodes without it are left out of that union. |
| `hooks` | `{ afterChange?, afterRead?, beforeChange?, beforeValidate? }` | None | Arrays of node-specific hooks. Each receives the serialized `node` and a `req` that exposes FrogBot as `req.frogbot`, and returns the node to save or read. |
| `validations` | `NodeValidation[]` | None | Validates serialized node data with the containing document. Return `true` when valid or an error message string. |
| `getSubFields` | `({ node, req }) => Field[] \| null` | None | Returns fields embedded in the node so their hooks and population behavior can run. |
| `getSubFieldsData` | `({ node, req }) => Record<string, unknown>` | None | Returns the serialized values corresponding to `getSubFields`. |
| `graphQLPopulationPromises` | `PopulationPromise[]` | None | Supplies depth-aware population callbacks to a separately integrated GraphQL runtime. |

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:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

export const DividerFeature = createServerFeature({
  feature: ({ featureProviderMap }) => {
    const fixedToolbarEnabled = featureProviderMap.has('toolbarFixed')

    return {
      i18n: {
        en: {
          label: fixedToolbarEnabled ? 'Insert divider' : 'Divider',
        },
      },
    }
  },
  key: 'divider',
})
```

`featureProviderMap` contains every configured feature, including features that have not loaded. Use `resolvedFeatures` when you need the output of an earlier feature.

| Argument | Type | Default | Description |
| - | - | - | - |
| `props` | `UnSanitizedProps` | May be `undefined` | Props passed when the feature was called, such as `DividerFeature({ style: 'dashed' })`. `undefined` when it was called with no argument. |
| `config` | `SanitizedConfig` | Always provided | The sanitized app config, including `collections` and `blocks`. |
| `featureProviderMap` | `Map<string, FeatureProviderServer>` | Always provided | Every feature configured in this editor, keyed by feature key, whether or not it has loaded. |
| `resolvedFeatures` | `Map<string, ResolvedServerFeature>` | Always provided | Features that have already loaded, keyed by feature key. |
| `unSanitizedEditorConfig` | `{ features: FeatureProviderServer[]; lexical: EditorConfig }` | Always provided | The editor's configured features and Lexical configuration before sanitization. |
| `isRoot` | `boolean` | May be `undefined` | `true` while loading the root `editor` from your FrogBot config. |
| `parentIsLocalized` | `boolean` | Always provided | Whether the rich text field sits inside a localized field. Pass it on when sanitizing nested fields. |

The dependency options on `createServerFeature` are listed in the [options table](#server-feature).

> [!WARNING]
>
> 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:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

export const DividerFeature = createServerFeature({
  feature: ({ featureProviderMap }) => {
    if (!featureProviderMap.has('paragraph')) {
      throw new Error('DividerFeature requires the paragraph feature')
    }

    return {}
  },
  key: 'divider',
})
```

## 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:

```ts theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'

export const DividerClientFeature = createClientFeature({})
```

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](#feature-props).

| Option | Type | Default | Description |
| - | - | - | - |
| `nodes` | `(Klass<LexicalNode> \| LexicalNodeReplacement)[]` | `[]` | Nodes registered in the editor. Register the same nodes in the server feature. See [Nodes](#client-feature-nodes). |
| `plugins` | `{ Component, position }[]` | `[]` | React components mounted inside the editor. See [Plugins](#plugins). |
| `toolbarInline` | `{ groups: ToolbarGroup[] }` | None | Groups added to the inline toolbar shown for selected text. See [Toolbar groups](#toolbar-groups). |
| `toolbarFixed` | `{ groups: ToolbarGroup[] }` | None | Groups added to the fixed toolbar. They appear only when the editor has `FixedToolbarFeature`. |
| `slashMenu` | `{ groups?: SlashMenuGroup[]; dynamicGroups?: ({ editor, queryString }) => SlashMenuGroup[] }` | None | Slash-menu entries. `groups` are always present and filtered by what the editor types after `/`. `dynamicGroups` is called again on every keystroke after `/`, and the groups it returns are merged with `groups`. See [Slash-menu groups](#slash-menu-groups). |
| `markdownTransformers` | `(Transformer \| ({ allNodes, allTransformers }) => Transformer)[]` | `[]` | Markdown shortcuts applied while editors type. See [Markdown transformers](#client-feature-markdown-transformers). |
| `providers` | `React.FC<{ children: JSX.Element }>[]` | `[]` | React context providers wrapped around the editor. See [Providers](#providers). |
| `enableFormats` | `TextFormatType[]` | `[]` | Text formats this feature allows, such as `'bold'`. The editor removes any format from text that no configured feature enables. |
| `sanitizedClientFeatureProps` | `ClientProps` | None | Only in the function form. Props stored on the resolved client feature and passed to its plugins as `clientProps`. Return them explicitly, as in the [client sanitization example](#feature-props); if omitted, plugins receive no props. The object form always uses the incoming props. |

Register the client feature from `feature.server.ts`:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

export const DividerFeature = createServerFeature({
  feature: {
    ClientFeature: './divider/feature.client#DividerClientFeature',
  },
  key: 'divider',
})
```

### 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`:

```tsx theme={"theme":"tokyo-night"}
import type {
  DOMConversionMap,
  DOMConversionOutput,
  DOMExportOutput,
  EditorConfig,
  LexicalNode,
  NodeKey,
  SerializedLexicalNode,
} from '@frogbotai/richtext-lexical/lexical'
import {
  $applyNodeReplacement,
  DecoratorNode,
} from '@frogbotai/richtext-lexical/lexical'
import { lazy, type ReactElement } from 'react'

export type SerializedDividerNode = SerializedLexicalNode & {
  fields: {
    title: string
  }
}

const DividerComponent = lazy(() =>
  import('../components/DividerComponent').then((module) => ({
    default: module.DividerComponent,
  })),
)

export class DividerNode extends DecoratorNode<ReactElement> {
  static clone(node: DividerNode): DividerNode {
    return new DividerNode(node.__title, node.__key)
  }

  static getType(): string {
    return 'divider'
  }

  static importDOM(): DOMConversionMap {
    return {
      hr: () => ({ conversion: convertDividerElement, priority: 0 }),
    }
  }

  static importJSON(serializedNode: SerializedDividerNode): DividerNode {
    return $createDividerNode(serializedNode.fields.title)
  }

  __title: string

  constructor(title = 'Divider', key?: NodeKey) {
    super(key)

    this.__title = title
  }

  createDOM(_config: EditorConfig): HTMLElement {
    return document.createElement('div')
  }

  decorate(): ReactElement {
    return <DividerComponent />
  }

  exportDOM(): DOMExportOutput {
    return { element: document.createElement('hr') }
  }

  exportJSON(): SerializedDividerNode {
    return {
      fields: { title: this.__title },
      type: 'divider',
      version: 1,
    }
  }

  getTextContent(): string {
    return '\n'
  }

  isInline(): false {
    return false
  }

  updateDOM(): false {
    return false
  }
}

function convertDividerElement(): DOMConversionOutput {
  return { node: $createDividerNode() }
}

export function $createDividerNode(title?: string): DividerNode {
  return $applyNodeReplacement(new DividerNode(title))
}

export function $isDividerNode(
  node: LexicalNode | null | undefined,
): node is DividerNode {
  return node instanceof DividerNode
}
```

`divider/components/DividerComponent.tsx`:

```tsx theme={"theme":"tokyo-night"}
'use client'

export function DividerComponent() {
  return <hr aria-label="Divider" />
}
```

Register the node on the client:

```ts theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'

import { DividerNode } from './nodes/DividerNode'

export const DividerClientFeature = createClientFeature({
  nodes: [DividerNode],
})
```

### Plugins

Plugins are React components mounted inside the editor's Lexical providers. They commonly register [commands](https://lexical.dev/docs/concepts/commands), listeners, or [node transforms](https://lexical.dev/docs/concepts/transforms).

`divider/plugin.tsx` registers a command that inserts the custom node:

```tsx theme={"theme":"tokyo-night"}
'use client'

import {
  $getSelection,
  $isRangeSelection,
  COMMAND_PRIORITY_EDITOR,
  createCommand,
  type LexicalCommand,
} from '@frogbotai/richtext-lexical/lexical'
import { useLexicalComposerContext } from '@frogbotai/richtext-lexical/lexical/react/LexicalComposerContext'
import { $insertNodeToNearestRoot } from '@frogbotai/richtext-lexical/lexical/utils'
import { useEffect } from 'react'

import { $createDividerNode } from './nodes/DividerNode'

export const INSERT_DIVIDER_COMMAND: LexicalCommand<void> = createCommand(
  'INSERT_DIVIDER_COMMAND',
)

export function DividerPlugin() {
  const [editor] = useLexicalComposerContext()

  useEffect(
    () =>
      editor.registerCommand(
        INSERT_DIVIDER_COMMAND,
        () => {
          const selection = $getSelection()

          if (!$isRangeSelection(selection)) {
            return false
          }

          $insertNodeToNearestRoot($createDividerNode())

          return true
        },
        COMMAND_PRIORITY_EDITOR,
      ),
    [editor],
  )

  return null
}
```

Plugins are registered as entries with a component and position:

```ts theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'

import { DividerNode } from './nodes/DividerNode'
import { DividerPlugin } from './plugin'

export const DividerClientFeature = createClientFeature({
  nodes: [DividerNode],
  plugins: [{ Component: DividerPlugin, position: 'normal' }],
})
```

| Option | Type | Default | Description |
| - | - | - | - |
| `Component` | `React.FC<{ clientProps }>` | Required | Plugin component. It receives the feature's sanitized client props as `clientProps`; `floatingAnchorElem` plugins also receive `anchorElem`. |
| `position` | `'aboveContainer' \| 'top' \| 'normal' \| 'bottom' \| 'belowContainer' \| 'floatingAnchorElem'` | Required | Where the component mounts. `aboveContainer` and `belowContainer` render outside the editor container. Inside it, `top` renders before the content, and `normal` and then `bottom` render after it. `floatingAnchorElem` renders with `anchorElem`, the element that floating UI such as menus is positioned against, and only once that element exists. |

### Toolbar groups

Toolbar groups are either horizontal icon buttons or dropdowns containing labeled items. Groups with the same key are merged.

```ts theme={"theme":"tokyo-night"}
import type {
  ToolbarGroup,
  ToolbarGroupItem,
} from '@frogbotai/richtext-lexical'

export function dividerButtonGroup(
  items: ToolbarGroupItem[],
): ToolbarGroup {
  return {
    items,
    key: 'dividerButtons',
    order: 30,
    type: 'buttons',
  }
}
```

| Option | Type | Default | Description |
| - | - | - | - |
| `key` | `string` | Required | Unique group identifier. Groups with the same key are merged: their items are combined and the other properties come from the first group registered. |
| `type` | `'buttons' \| 'dropdown'` | Required | `buttons` shows items horizontally as icons. `dropdown` shows items vertically with icons and labels when opened. |
| `items` | `ToolbarGroupItem[]` | Required | Toolbar items in the group. See [Toolbar items](#toolbar-items). |
| `order` | `number` | Unordered | Position relative to other groups, in ascending order. Groups without `order` come after ordered ones. |
| `ChildComponent` | `React.FC` | None | Dropdown groups only. Component shown on the dropdown trigger, usually an icon. |
| `isEnabled` | `({ editor, editorConfigContext, selection }) => boolean` | Always enabled | Dropdown groups only. Return `false` to disable the dropdown trigger for the current selection. |
| `iconOnly` | `boolean` | `false` | Dropdown groups only. Shows the items as icons, without labels, with a tooltip on hover. |
| `maxActiveItems` | `number` | `1` | Dropdown groups only. Maximum number of items marked active at once. The editor stops calling `isActive` for the remaining items once this many are active, so higher values cost more work. |

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:

```tsx theme={"theme":"tokyo-night"}
'use client'

import {
  createClientFeature,
  toolbarAddDropdownGroupWithItems,
} from '@frogbotai/richtext-lexical/client'
import { $isNodeSelection } from '@frogbotai/richtext-lexical/lexical'

import { DividerIcon } from './components/DividerIcon'
import { $isDividerNode, DividerNode } from './nodes/DividerNode'
import { DividerPlugin, INSERT_DIVIDER_COMMAND } from './plugin'

const dividerGroups = [
  toolbarAddDropdownGroupWithItems([
    {
      ChildComponent: DividerIcon,
      isActive: ({ selection }) => {
        if (!$isNodeSelection(selection)) {
          return false
        }

        return selection.getNodes().some($isDividerNode)
      },
      key: 'divider',
      label: ({ i18n }) => i18n.t('lexical:divider:label'),
      onSelect: ({ editor }) => {
        editor.dispatchCommand(INSERT_DIVIDER_COMMAND, undefined)
      },
    },
  ]),
]

export const DividerClientFeature = createClientFeature({
  nodes: [DividerNode],
  plugins: [{ Component: DividerPlugin, position: 'normal' }],
  toolbarFixed: { groups: dividerGroups },
  toolbarInline: { groups: dividerGroups },
})
```

`divider/components/DividerIcon.tsx`:

```tsx theme={"theme":"tokyo-night"}
'use client'

export function DividerIcon() {
  return <span aria-hidden="true">+</span>
}
```

| Option | Type | Default | Description |
| - | - | - | - |
| `key` | `string` | Required | Unique item identifier. |
| `ChildComponent` | `React.FC` | None | Content inside the default toolbar button, usually an icon. |
| `Component` | `React.FC<{ active?, anchorElem, editor, enabled?, item }>` | None | Replaces the default button; when present, `ChildComponent` and `onSelect` are ignored. |
| `label` | `string \| ({ featureClientSchemaMap, i18n, schemaPath }) => string` | None | Label shown when the item is in a dropdown group. Use a function for translated text. |
| `onSelect` | `({ editor, isActive }) => void` | None | Handles selection of the item. |
| `isActive` | `({ editor, editorConfigContext, selection }) => boolean` | Never active | Controls whether the item is highlighted for the current selection. |
| `isEnabled` | `({ editor, editorConfigContext, selection }) => boolean` | Always enabled | Return `false` to gray out the item and make it unclickable. |
| `order` | `number` | Unordered | Position within the group, in ascending order. Items without `order` come after ordered ones. |

### Slash-menu groups

Slash-menu groups with the same key are merged:

```ts theme={"theme":"tokyo-night"}
import type {
  SlashMenuGroup,
  SlashMenuItem,
} from '@frogbotai/richtext-lexical'

export function dividerSlashMenuGroup(
  items: SlashMenuItem[],
): SlashMenuGroup {
  return {
    items,
    key: 'layout',
    label: 'Layout',
  }
}
```

Use the exported `slashMenuBasicGroupWithItems` helper to add entries to the standard Basic group.

| Option | Type | Default | Description |
| - | - | - | - |
| `key` | `string` | Required | Unique group identifier. Groups with the same key are merged: their items are combined and the label comes from the first group registered. |
| `items` | `SlashMenuItem[]` | Required | Items in the group. See [Slash-menu items](#slash-menu-items). |
| `label` | `string \| ({ featureClientSchemaMap, i18n, schemaPath }) => string` | Group `key` | Group heading in the menu. Use a function for translated text. |

### Slash-menu items

```tsx theme={"theme":"tokyo-night"}
'use client'

import {
  createClientFeature,
  slashMenuBasicGroupWithItems,
} from '@frogbotai/richtext-lexical/client'

import { DividerIcon } from './components/DividerIcon'
import { DividerNode } from './nodes/DividerNode'
import { DividerPlugin, INSERT_DIVIDER_COMMAND } from './plugin'

export const DividerClientFeature = createClientFeature({
  nodes: [DividerNode],
  plugins: [{ Component: DividerPlugin, position: 'normal' }],
  slashMenu: {
    groups: [
      slashMenuBasicGroupWithItems([
        {
          Icon: DividerIcon,
          key: 'divider',
          keywords: ['divider', 'line', 'separator'],
          label: ({ i18n }) => i18n.t('lexical:divider:label'),
          onSelect: ({ editor }) => {
            editor.dispatchCommand(INSERT_DIVIDER_COMMAND, undefined)
          },
        },
      ]),
    ],
  },
})
```

| Option | Type | Default | Description |
| - | - | - | - |
| `key` | `string` | Required | Unique item identifier. Also used as the displayed and searched text when `label` is not set. |
| `Icon` | `React.FC` | Required | Component rendered beside the item. |
| `onSelect` | `({ editor, queryString }) => void` | Required | Handles selection of the item. `queryString` is the text typed after `/`. |
| `label` | `string \| ({ featureClientSchemaMap, i18n, schemaPath }) => string` | Item `key` | Displayed text, either static or translated. It is matched against the text typed after `/` and shortened to 25 characters in the menu. |
| `keywords` | `string[]` | None | Additional terms matched against the text typed after `/`. |

### 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:

```ts theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'

import { DividerMarkdownTransformer } from './markdownTransformer'
import { DividerNode } from './nodes/DividerNode'

export const DividerClientFeature = createClientFeature({
  markdownTransformers: [DividerMarkdownTransformer],
  nodes: [DividerNode],
})
```

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`:

```tsx theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'
import { createContext, type ReactNode } from 'react'

export const DividerStyleContext = createContext<'solid' | 'dashed'>('solid')

function DividerStyleProvider({ children }: { children: ReactNode }) {
  return (
    <DividerStyleContext.Provider value="solid">
      {children}
    </DividerStyleContext.Provider>
  )
}

export const DividerClientFeature = createClientFeature({
  providers: [DividerStyleProvider],
})
```

## 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`:

```ts theme={"theme":"tokyo-night"}
import { createServerFeature } from '@frogbotai/richtext-lexical'

type DividerFeatureInput = {
  style?: 'solid' | 'dashed'
}

type DividerFeatureProps = {
  style: 'solid' | 'dashed'
}

type DividerClientProps = DividerFeatureProps

export const DividerFeature = createServerFeature<
  DividerFeatureInput,
  DividerFeatureProps,
  DividerClientProps
>({
  feature: ({ props }) => {
    const sanitizedProps: DividerFeatureProps = {
      style: props?.style ?? 'solid',
    }

    return {
      ClientFeature: './divider/feature.client#DividerClientFeature',
      clientFeatureProps: sanitizedProps,
      sanitizedServerFeatureProps: sanitizedProps,
    }
  },
  key: 'divider',
})
```

Client sanitization follows the same pattern:

```ts theme={"theme":"tokyo-night"}
'use client'

import { createClientFeature } from '@frogbotai/richtext-lexical/client'

type DividerClientInput = {
  style?: 'solid' | 'dashed'
}

type DividerClientProps = {
  style: 'solid' | 'dashed'
}

export const DividerClientFeature = createClientFeature<
  DividerClientInput,
  DividerClientProps
>(({ props }) => {
  const sanitizedProps = {
    ...props,
    style: props.style ?? 'solid',
  }

  return {
    sanitizedClientFeatureProps: sanitizedProps,
  }
})
```

The client feature function receives:

| Argument | Type | Default | Description |
| - | - | - | - |
| `props` | `ClientInput & { featureKey: string; order: number }` | Always provided | The `clientFeatureProps` sent by the server feature, plus the feature's key and its position in the editor's feature list. |
| `config` | `ClientConfig` | Always provided | The serializable client config. |
| `field` | `RichTextFieldClient` | May be `undefined` | Client configuration of the rich text field being rendered. |
| `schemaPath` | `string` | Always provided | Schema path of the rich text field. |
| `featureProviderMap` | `Map<string, FeatureProviderClient>` | Always provided | Every client feature configured in this editor, keyed by feature key. |
| `resolvedFeatures` | `Map<string, ResolvedClientFeature>` | Always provided | Client features that have already loaded. |
| `unSanitizedEditorConfig` | `ClientEditorConfig` | Always provided | The editor's client features and Lexical configuration before sanitization. |
| `featureClientSchemaMap` | `Record<string, Record<string, ClientField[]>>` | Always provided | Client field schemas registered by features through `generateSchemaMap`, keyed by feature key. |
| `featureClientImportMap` | `Record<string, unknown>` | Always provided | Components added by object-form `componentImports` on server features, keyed `<featureKey>.<key>`. |

### 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.