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

# Views (Experimental)

> Customize rich text rendering in the Admin Panel and JSX converters.

> [!WARNING]
>
> This feature is experimental and may change or be removed in future releases.

Views define named rendering modes for a rich text field. Editors can switch views in the Admin Panel without changing the saved editor state, and a view's node map can also render that state through a JSX converter.

## Views and JSX converters

[JSX converters](https://docs.frogbot.ai/rich-text/converting-jsx.md) are the simplest choice when content only needs custom frontend rendering. Views are useful when you want an Admin Panel preview, multiple rendering modes for one field, or one node map shared by the editor and frontend.

When both `converters` and `nodeMap` are passed to `RichText`, the node map wins for each overlapping node type. Block and inline-block maps are merged by slug, with the node map winning for an overlapping slug.

## Define views

The following view map uses real node narrowing and declares every custom block represented by its node union:

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

import type {
  DefaultNodeTypes,
  LexicalEditorViewMap,
  SerializedBlockNode,
  SerializedInlineBlockNode,
} from '@frogbotai/richtext-lexical'
import { $isHeadingNode } from '@frogbotai/richtext-lexical/lexical/rich-text'

type BannerBlock = {
  blockName?: string
  blockType: 'banner'
  message: string
}

type BadgeBlock = {
  blockName?: string
  blockType: 'badge'
  label: string
}

export type PostNodeTypes =
  | DefaultNodeTypes
  | SerializedBlockNode<BannerBlock>
  | SerializedInlineBlockNode<BadgeBlock>

function getEditorField(node: unknown, name: string): string {
  if (typeof node !== 'object' || node === null || !('__fields' in node)) {
    return ''
  }

  const fields = node.__fields

  if (typeof fields !== 'object' || fields === null) {
    return ''
  }

  return String(Reflect.get(fields, name) ?? '')
}

export const postViews: LexicalEditorViewMap<PostNodeTypes> = {
  default: {
    nodes: {},
  },
  preview: {
    admin: {
      hideGutter: true,
    },
    filterFeatures: (features) => {
      const { toolbarFixed, toolbarInline, ...previewFeatures } = features

      return previewFeatures
    },
    lexical: (defaultConfig) => ({
      ...defaultConfig,
      theme: {
        ...defaultConfig.theme,
        link: 'post-preview-link',
        paragraph: 'post-preview-paragraph',
      },
    }),
    nodes: {
      heading: {
        createDOM: ({ node }) => {
          if (!$isHeadingNode(node)) {
            return document.createElement('div')
          }

          const heading = document.createElement(node.getTag())

          heading.className = 'post-preview-heading'

          return heading
        },
      },
      blocks: {
        banner: {
          Component: ({ node, isEditor }) => {
            const message = isEditor ? getEditorField(node, 'message') : node.fields.message

            return <aside className="post-preview-banner">{message}</aside>
          },
        },
      },
      inlineBlocks: {
        badge: {
          Component: ({ node, isEditor }) => {
            const label = isEditor ? getEditorField(node, 'label') : node.fields.label

            return <span className="post-preview-badge">{label}</span>
          },
        },
      },
    },
  },
}
```

Reference the named export from the rich text field. The path follows the same [component path rules](https://docs.frogbot.ai/custom-components/overview.md#component-paths) as other Admin Panel components.

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

export const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [
          ...defaultFeatures,
          BlocksFeature({
            blocks: [
              {
                slug: 'banner',
                fields: [{ name: 'message', type: 'text', required: true }],
              },
            ],
            inlineBlocks: [
              {
                slug: 'badge',
                fields: [{ name: 'label', type: 'text', required: true }],
              },
            ],
          }),
        ],
        views: '/collections/Posts/views#postViews',
      }),
    },
  ],
}
```

When views are configured, the Admin Panel displays a selector beside the field label. Selecting another view changes rendering and editor configuration, not the underlying content.

## View configuration

Each key of the view map is a view name. Add a `default` key to replace the editor's default rendering; other keys add views editors can switch to. Each view accepts:

| Option | Type | Default | Description |
| - | - | - | - |
| `nodes` | `LexicalEditorNodeMap` | Required | Rendering overrides keyed by node type, with custom blocks under `blocks` and `inlineBlocks` keyed by slug. See [Node overrides](#node-overrides). Use `{}` to keep the default rendering. |
| `admin` | `LexicalFieldAdminClientProps` | The editor's `admin` options | Admin options used while this view is active, replacing the editor's own `admin` options. See [Admin settings](#admin-settings). |
| `filterFeatures` | `(features: ClientFeaturesMap) => ClientFeaturesMap` | All features | Receives the client features keyed by feature key and returns the features to use in this view. See [Feature filtering](#feature-filtering). |
| `lexical` | `EditorConfig \| (defaultConfig: EditorConfig) => EditorConfig` | The editor's Lexical configuration | Lexical configuration for this view. See [Lexical configuration](#lexical-configuration). |

### Admin settings

View-level admin settings use the same options as [rich text admin customization](https://docs.frogbot.ai/rich-text/overview.md#admin-customization), except that `placeholder` must be a plain string because the view map is loaded in the browser:

```tsx theme={"theme":"tokyo-night"}
{
  preview: {
    admin: {
      hideAddBlockButton: true,
      hideDraggableBlockElement: true,
      hideGutter: true,
      hideInsertParagraphAtEnd: true,
      placeholder: 'Preview content',
    },
  },
}
```

### Feature filtering

`filterFeatures` receives the active client feature map. Return a new map to remove or add features for that view. Removing toolbar features is useful for a read-focused preview, as shown in the complete map above.

### Lexical configuration

Pass a function to extend the default Lexical editor configuration:

```tsx theme={"theme":"tokyo-night"}
{
  preview: {
    lexical: (defaultConfig) => ({
      ...defaultConfig,
      theme: {
        ...defaultConfig.theme,
        paragraph: 'post-preview-paragraph',
      },
    }),
  },
}
```

Pass an object when the view should replace the configuration entirely:

```tsx theme={"theme":"tokyo-night"}
{
  minimal: {
    lexical: {
      namespace: 'post-minimal-view',
      theme: {
        paragraph: 'post-minimal-paragraph',
      },
    },
  },
}
```

### Node overrides

A node override can use these rendering approaches:

| Option | Type | Default | Description |
| - | - | - | - |
| `Component` | `(props) => ReactNode` | None | Renders a React component in the JSX converter, and in the Admin Panel for decorator nodes. Use `isEditor` to narrow the props: the editor variant receives the Lexical `node`, `editor`, and `config`; the converter variant receives the serialized `node` and the converter arguments. Takes priority over `html`. |
| `createDOM` | `({ node, editor, config }) => HTMLElement` | None | Admin Panel only. Creates the DOM element for a Lexical editor node. Narrow the node before calling node-specific methods. |
| `html` | `(props) => string` | None | Returns an HTML string, in both the Admin Panel and the JSX converter. Receives the same `isEditor`-narrowed props as `Component`. |
| `Block` | `React.FC<ViewMapBlockComponentProps>` | None | Entries under `blocks` and `inlineBlocks` only. Replaces the block's content in both the Admin Panel and the JSX converter; see [Customize block chrome](#customize-block-chrome). In the converter, `Component` takes priority over it. |
| `Label` | `React.FC<ViewMapBlockComponentProps>` | None | Entries under `blocks` and `inlineBlocks` only. Admin Panel only. Replaces the collapsible block label. |

For a decorator node that defines both `createDOM` and `html`, the editor uses `createDOM`; JSX conversion uses `html`.

The node map follows the organization described in [Converting JSX](https://docs.frogbot.ai/rich-text/converting-jsx.md): built-in node types are top-level keys, while custom block slugs belong under `blocks` and `inlineBlocks`.

## Customize block chrome

For a configured block, `Block` replaces the block's content while retaining its surrounding form behavior. Narrow `props.isEditor` before calling `useBlockComponentContext`; that hook is available only in the editor variant.

```tsx theme={"theme":"tokyo-night"}
import type {
  SerializedBlockNode,
  ViewMapBlockComponentProps,
} from '@frogbotai/richtext-lexical'

type BannerBlock = {
  blockName?: string
  blockType: 'banner'
  message: string
}

type BannerNode = SerializedBlockNode<BannerBlock>

export function BannerViewBlock(props: ViewMapBlockComponentProps<BannerNode>) {
  if (props.isEditor) {
    const { BlockCollapsible, EditButton, RemoveButton } =
      props.useBlockComponentContext()

    return (
      <BlockCollapsible>
        <p>{props.formData.message}</p>
        <EditButton />
        <RemoveButton />
      </BlockCollapsible>
    )
  }

  return <aside className="post-banner">{props.formData.message}</aside>
}
```

Assign it to `nodes.blocks.banner.Block`. `Label` replaces only the collapsible block label and is used only in the editor. The equivalent collection-level block customization is documented under [Block custom components](https://docs.frogbot.ai/fields/blocks.md#custom-components).

`ViewMapBlockComponentProps` is a union narrowed by `isEditor`. `Block` and `Label` components for inline blocks receive the same props, except that the editor variant provides `useInlineBlockComponentContext` instead of `useBlockComponentContext`.

| Prop | Type | Default | Description |
| - | - | - | - |
| `isEditor` | `boolean` | Always provided | `true` in the Admin Panel editor, `false` in the JSX converter. |
| `isJSXConverter` | `boolean` | Always provided | The opposite of `isEditor`. |
| `formData` | `BlockFields` | Always provided | The block's field values. |
| `useBlockComponentContext` | `() => BlockComponentContextType` | Editor only | Hook that returns the editor's block controls, such as `BlockCollapsible`, `EditButton`, and `RemoveButton`. Call it inside the component. |
| `nodeKey` | `string` | Editor only | Lexical key of the block node. |
| `className` | `string` | Editor only | Class name of the default block container. |
| `node` | `SerializedBlockNode` | Converter only | The serialized block node. |
| `nodesToJSX` | `({ nodes, converters?, parent?, disableIndent?, disableTextAlign? }) => ReactNode[]` | Converter only | Converts child nodes to JSX. |
| `converters` | `JSXConverters` | Converter only | The converters in use, for rendering nested content. |
| `parent` | `SerializedLexicalNode` | Converter only | The parent node. |
| `childIndex` | `number` | Converter only | Position of the block among its siblings. |

> [!WARNING]
>
> A top-level import is included in every bundle that imports the view map. An
> `isEditor` branch controls runtime behavior but does not remove an editor-only
> top-level import. Keep shared view files free of imports from
> `@frogbotai/richtext-lexical/client` and `@frogbotai/ui/admin`; use the context hooks
> supplied through narrowed component props instead.

## Read the active view

Client components rendered within a rich text field can read the current view:

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

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

export function ActiveView() {
  const { currentView, views } = useRichTextView()

  return (
    <p>
      {currentView} ({Object.keys(views ?? {}).length} views)
      {views?.[currentView]?.admin?.hideGutter ? ', gutter hidden' : ''}
    </p>
  )
}
```

| Property | Type | Default | Description |
| - | - | - | - |
| `currentView` | `string` | Always provided | Name of the active view. Falls back to `'default'` when the requested view is not in `views`. |
| `setCurrentView` | `(view: string) => void` | Always provided | Switches the active view. |
| `views` | `LexicalEditorViewMap` | May be `undefined` | Every view configured for the editor. |
| `currentViewMap` | `LexicalEditorNodeMap` | May be `undefined` | The active view's entry from `views`. Although typed as a node map, it holds the whole entry (`admin`, `nodes`, and so on); use `views?.[currentView]?.nodes` for the node map. |
| `inheritable` | `boolean` | May be `undefined` | Whether nested rich text editors inherit this view state. |
| `isControlledByParent` | `boolean` | May be `undefined` | `true` when an ancestor editor controls the view. The view selector is hidden in that case. |
| `hasExplicitCurrentView` | `boolean` | May be `undefined` | `true` when the view was set explicitly rather than defaulting to `'default'`. |

## Reuse a view on the frontend

Pass a view's node map to `RichText` to use the same rendering outside the editor. Type the content with the generated field type from `frogbot-types`.

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

import { RichText } from '@frogbotai/richtext-lexical/react'
import type { Post } from '@/frogbot-types'

import { postViews } from './views'

export function PostBody({ content }: { content: Post['content'] }) {
  if (!content) {
    return null
  }

  return <RichText data={content} nodeMap={postViews.preview.nodes} />
}
```

`PostBody` is a Client Component because it reads properties of the client-exported `postViews` map. A Server Component can render `<PostBody content={content} />`, but cannot read `postViews.preview.nodes` itself.

The frontend imports the entire module containing `postViews`. Keep that module safe for the frontend bundle and move editor-only components into separate component paths when they require client editor or Admin Panel dependencies.


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