Skip to content
Froala Documentation

Migrate from Lexical to Froala

This guide covers the latest stable Lexical 0.47 and the Froala WYSIWYG Editor v5. You get side-by-side code for every migration step (initialization, configuration, plugins, content, events, methods, image upload, and React) so you can move a working editor over with confidence.

What you need to know first

Lexical and Froala solve the same problem from opposite directions. Lexical is a headless, framework-agnostic editor engine: its source of truth is a serializable EditorState node tree, not the DOM, and you assemble every feature from small modular packages while building your own toolbar and UI. Froala is a complete WYSIWYG editor: one constructor gives you a styled toolbar, 30+ plugins, and a document model where HTML is the source of truth. In short, Lexical hands you primitives to build an editor; Froala hands you the editor.

Licensing also differs. Lexical is MIT-licensed and free to use. Froala v5 is a commercial product that you activate with a license key option. You will need a new v5 key, which you can request from the Froala team.

Concept Lexical 0.47 Froala v5
Initialization createEditor(config) + setRootElement() new FroalaEditor('#editor', options)
Element targeting A contentEditable element you own CSS selector or DOM node: div, textarea, span, even an img
Enabling features Import modular packages, register nodes and plugins 30+ pluginsEnabled plugins
Toolbar Build your own; dispatch commands like FORMAT_TEXT_COMMAND toolbarButtons array of button groups
Reading content $generateHtmlFromNodes(editor, null) editor.html.get()
Writing content $generateNodesFromDOM() + $insertNodes() editor.html.set(html)
Events editor.registerUpdateListener(cb) events: { contentChanged: … }
Teardown editor.setRootElement(null) + run listener cleanups editor.destroy()
Licensing MIT, open source (free) Commercial license key (key option)

The one mental shift: stop thinking in a node tree that you serialize and assemble feature-by-feature, and start thinking HTML in, HTML out. In Lexical you read and write an EditorState and wire behavior through commands and plugins. In Froala the content is HTML (html.get() returns it and html.set() replaces it), and the toolbar and plugins are already wired for you.

Swap Lexical for Froala in three steps

Remove the Lexical packages and setup, add Froala, then initialize. The container's existing HTML becomes Froala's starting content, so you rarely need a separate "set content" step to get going.

Step 1: Remove Lexical

Uninstall the Lexical packages and delete the engine setup: the createEditor call, the setRootElement wiring, and the register* behavior calls (or the <LexicalComposer> tree in React).

// Uninstall the Lexical packages
// npm uninstall lexical @lexical/rich-text @lexical/html @lexical/list @lexical/link

// Delete the engine setup
import {createEditor} from 'lexical';
import {registerRichText} from '@lexical/rich-text';

const editor = createEditor({ namespace: 'MyEditor', onError });
editor.setRootElement(document.getElementById('editor'));
registerRichText(editor);

Step 2 — Add Froala

Add the Froala stylesheet and script. The .pkgd build bundles the core editor plus all official plugins, so everything is available immediately. Use the CDN for a drop-in include, or npm for a bundled app.

<!-- CDN: bundled build with all plugins -->
<link href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>

# Or install from npm
# npm install froala-editor

Step 3 — Initialize the editor

Replace the Lexical engine wiring with a single constructor call. Pass your commercial license key. Froala accepts a CSS selector or a DOM node, and it can attach to a div, textarea, or other elements. Whatever is already in your markup becomes the initial content.

const editor = createEditor({ namespace: 'MyEditor', onError });
editor.setRootElement(document.getElementById('editor'));
registerRichText(editor); // wire basic rich-text behavior
const editor = new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY'
});

Configuration and options

In Lexical, configuration is split: the createEditor config carries the namespace, theme, registered nodes, and error handler, while feature behavior lives in the plugins you add. In Froala, a single options object controls everything: over 100 options for sizing, placeholder text, toolbar layout, counters, and more.

const editor = createEditor({
  namespace: 'MyEditor',
  editable: true,
  theme: { paragraph: 'my-paragraph', text: { bold: 'my-bold' } },
  nodes: [HeadingNode, ListNode, ListItemNode, LinkNode],
  onError: (error) => console.error(error),
});
const editor = new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY',
  heightMin: 300,
  placeholderText: 'Start typing…',
  charCounterCount: true,
  theme: 'gray',
});

See the full Options reference for every available setting.

Plugins

Lexical features ship as separate packages: you import each one, register any custom nodes it needs in the config, and wire its behavior. Froala bundles its plugins in the .pkgd build, so you turn features on with pluginsEnabled, or start from everything and switch a few off with the inverse pluginsDisabled.

import {ListNode, ListItemNode, registerList} from '@lexical/list';
import {LinkNode} from '@lexical/link';

const editor = createEditor({
  namespace: 'MyEditor',
  nodes: [ListNode, ListItemNode, LinkNode],
  onError,
});
registerRichText(editor);
registerList(editor);
// Enable only the plugins you need
new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY',
  pluginsEnabled: ['align', 'lists', 'link', 'image', 'table'],
});

// Or start from everything and disable a few
new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY',
  pluginsDisabled: ['video', 'emoticons'],
});

Getting and setting content

Reading and writing content is where the "HTML in, HTML out" shift pays off. Lexical works through its state model: you read HTML with $generateHtmlFromNodes inside editor.read(), and you write it by parsing a DOM, generating nodes, and inserting them inside editor.update(). Froala exposes the HTML directly.

Reading content

import {$generateHtmlFromNodes} from '@lexical/html';

let html = '';
editor.read(() => {
  html = $generateHtmlFromNodes(editor, null);
});
const html = editor.html.get();

Writing content

import {$generateNodesFromDOM} from '@lexical/html';
import {$getRoot, $insertNodes} from 'lexical';

editor.update(() => {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().clear();
  $getRoot().select();
  $insertNodes(nodes);
});
editor.html.set('<p>Hello <strong>Froala</strong>!</p>');

Events

Lexical notifies you of changes through registerUpdateListener, which returns a cleanup function and hands you the new editorState to read. Froala uses a declarative events map on the options object, and inside each handler this is the editor instance.

const removeListener = editor.registerUpdateListener(({editorState}) => {
  editorState.read(() => {
    const html = $generateHtmlFromNodes(editor, null);
    console.log('Content changed:', html);
  });
});

// Call when you no longer need it
removeListener();
new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY',
  events: {
    initialized: function () {
      console.log('Editor ready');
    },
    contentChanged: function () {
      console.log('Content changed:', this.html.get());
    },
  },
});

Browse the full list on the Events reference.

Methods

Common imperative actions (toggling read-only mode, focusing, and tearing down) map cleanly between the two editors. Lexical exposes these on the editor instance; Froala groups them into namespaced modules.

editor.setEditable(false);   // read-only
editor.setEditable(true);    // editable
editor.focus();              // focus
editor.blur();               // blur
editor.setRootElement(null); // tear down (also run listener cleanups)
editor.edit.off();      // read-only
editor.edit.on();       // editable
editor.events.focus();  // focus
editor.destroy();        // tear down

See the complete Methods reference.

Image upload

Lexical ships no image upload out of the box. You register a custom image node and command, then handle the upload yourself. Froala includes a full image plugin: point it at your endpoint with a few options, and your server returns a small JSON payload. When an image is inserted, Froala automatically sends a request to imageUploadURL; your server saves the file and responds with { "link": "/path/to/image.jpg" }.

// Lexical has no built-in image upload.
// Register a custom command + node and upload yourself.
import {createCommand, COMMAND_PRIORITY_EDITOR} from 'lexical';

export const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE');

editor.registerCommand(INSERT_IMAGE_COMMAND, (payload) => {
  // Upload payload.file, then insert your custom ImageNode
  return true;
}, COMMAND_PRIORITY_EDITOR);
new FroalaEditor('#editor', {
  key: 'YOUR_FROALA_LICENSE_KEY',
  imageUploadURL: '/upload_image',
  imageUploadParam: 'file',
  imageUploadMethod: 'POST',
  imageMaxSize: 5 * 1024 * 1024,
  imageAllowedTypes: ['jpeg', 'jpg', 'png'],
  events: {
    'image.uploaded': function (response) {
      // Server returns JSON: { "link": "/path/to/image.jpg" }
    },
    'image.error': function (error, response) {
      // Handle upload errors
    },
  },
});

Framework integration: React

In React, Lexical uses <LexicalComposer> with plugin components as children: you compose the editor from RichTextPlugin, HistoryPlugin, OnChangePlugin, and others. Froala provides a single FroalaEditorComponent with two-way binding through model and onModelChange. Install it with npm install react-froala-wysiwyg froala-editor.

import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';

const initialConfig = { namespace: 'MyEditor', onError: (e) => console.error(e) };

function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
      <OnChangePlugin onChange={(state) => { /* … */ }} />
    </LexicalComposer>
  );
}
import {useState} from 'react';
import FroalaEditorComponent from 'react-froala-wysiwyg';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/css/froala_editor.pkgd.min.css';

function Editor() {
  const [model, setModel] = useState('<p>Hello Froala!</p>');
  return (
    <FroalaEditorComponent
      tag="textarea"
      model={model}
      onModelChange={setModel}
      config={{ key: 'YOUR_FROALA_LICENSE_KEY', heightMin: 300 }}
    />
  );
}

Froala also ships first-party SDKs for Angular and Vue.

Feature mapping reference

Use this table to find the Froala equivalent of a Lexical feature at a glance.

Feature Lexical 0.47 Froala v5
Bold / italic / underline dispatchCommand(FORMAT_TEXT_COMMAND, 'bold') bold, italic, underline toolbar buttons
Headings HeadingNode (@lexical/rich-text) paragraphFormat
Lists @lexical/list lists plugin (formatUL, formatOL)
Links @lexical/link link plugin
Tables @lexical/table table plugin
Undo / redo <HistoryPlugin /> (@lexical/history) Built-in undo, redo
Markdown @lexical/markdown markdown plugin
HTML import / export @lexical/html html.get() / html.set()
Read-only editor.setEditable(false) editor.edit.off()
Serialized state editorState.toJSON() (JSON node tree) HTML string

You're ready to ship

You’ve swapped the engine, mapped configuration and plugins, and matched content, events, methods, image upload, and React. Explore these references to fine-tune your Froala setup.

Do you think we can improve this article? Let us know.

Ready to dive in? Explore our plans