Migration guide

Migrate from TinyMCE, CKEditor, Froala, Quill, Lexical, or Tiptap

Most teams finish in under a day. Existing HTML content keeps working, toolbar strings map mostly 1→1, and the features you’re paying a premium tier for ship in our base license. Below: side-by-side config mapping, feature-equivalence tables, and a step-by-step checklist for each source editor.

~1 daytypical migration time for a single-app deployment
$0 / yearperpetual license after the one-time purchase
0 features lostfull collab + AI stack ships in the base license

Why teams move

  • Perpetual license. No subscription, no per-load metering, no AI usage-credit invoices.
  • One SKU for everything. Track Changes, Comments, Revision History, Mentions, AI — all in the base license. TinyMCE bills these separately.
  • Self-host on the entry tier. TinyMCE gates self-hosting to Enterprise.
  • No proxy for AI. TinyMCE AI requires a server proxy. Our editor.aiToolkit.setResolver(fn) is client-side; for ASP.NET Core use IRichTextBoxAiResolver.

Step-by-step checklist

  1. Install the npm package: npm install @@richscripts2/richtexteditor
  2. Replace your TinyMCE <script src="tinymce.min.js"> with <link rel="stylesheet" href="richtexteditor/rte_theme_default.css"> + <script src="richtexteditor/rte.js">.
  3. Remove the TinyMCE init script (tinymce.init({ selector: ... })) and replace the <textarea> with <div id="editor">.
  4. Instantiate: new RichTextEditor("#editor", { toolbar: "full" });
  5. Map your TinyMCE plugins: and toolbar: to our toolbar config — see the cheat sheet below.
  6. Wire your AI resolver via editor.aiToolkit.setResolver(async (req) => { ... }). No proxy needed.
  7. Existing HTML content keeps working; submit the form as normal.

Config cheat sheet

Basic setup

TinyMCE

tinymce.init({
  selector: '#editor',
  plugins: 'lists link image table code',
  toolbar: 'bold italic | bullist numlist | link image',
  height: 500
});

RichTextEditor

new RichTextEditor("#editor", {
  toolbar: "custom",
  toolbar_custom:
    "{bold,italic}|{insertunorderedlist,insertorderedlist}"
    + "|{insertlink,insertimage}",
  height: "500px"
});

AI integration

TinyMCE AI (requires proxy)

tinymce.init({
  plugins: 'ai',
  toolbar: 'ai',
  ai_request: (request, respondWith) =>
    respondWith.stream(/* proxy fetch */)
});

RichTextEditor (client-side resolver)

var editor = new RichTextEditor("#editor", {
  aiToolkitEnabled: true
});

editor.aiToolkit.setResolver(async (req) => {
  const r = await fetch("/api/my-ai", {
    method: "POST",
    body: JSON.stringify(req)
  });
  return r.json();
});

Track Changes / Comments / Revision History

Three separate TinyMCE premium add-ons → three config flags.

TinyMCE (premium)

tinymce.init({
  plugins: 'tinycomments revisionhistory',
  tinycomments_author: 'User',
  // Track Changes = separate license
});

RichTextEditor (base)

new RichTextEditor("#editor", {
  trackChangesEnabled: true,
  commentsEnabled: true,
  revisionHistoryEnabled: true,
  currentUser: {
    id: "maya",
    name: "Maya Patel",
    color: "#9333ea"
  }
});

Plugin equivalence

TinyMCE pluginRichTextEditor equivalent
linkbuilt-in (insertlink)
image, imagetoolsbuilt-in (insertimage, imageeditor)
mediabuilt-in (insertvideo, insertyoutube)
tablebuilt-in (inserttable)
lists, advlistbuilt-in (insertorderedlist, insertunorderedlist, insertchecklist)
codesamplebuilt-in (insertcode, syntaxhighlighter)
emoticonsbuilt-in (insertemoji)
templatebuilt-in (inserttemplate)
mentions (premium)mentionEnabled: true (base)
tinycomments (premium)commentsEnabled: true (base)
tinymcespellchecker (premium)native browser spellcheck
revisionhistory (premium)revisionHistoryEnabled: true (base)
Track Changes (premium)trackChangesEnabled: true (base)
ai (premium + usage credits)aiToolkitEnabled: true + resolver
exportword (premium)editor.aiToolkit.exportDocx() helper (when server endpoint is wired)
exportpdf (premium)html2pdf toolbar item (client-side)

Behavior differences to know about

HTML output. TinyMCE output uses lower-case element names and non-breaking spaces in empty paragraphs. Our output is case-normalized but otherwise equivalent. Customize via config.filterhtml.
Events. TinyMCE’s change event → editor.attachEvent("change", fn). Our new RichTextEditor(...) is synchronous; bind events right after construction.
Content CSS. TinyMCE content_css: "..." → our config.contentCssUrl (URL) or config.contentCssText (inline).

What you gain

  • Dictation plugin — TinyMCE doesn’t ship one.
  • Slash commands (Notion-style / picker) — more mature than TinyMCE’s autocompleter API.
  • Shared review ledger — AI suggestions + human track-changes + comments in one drawer. TinyMCE keeps these siloed.
  • React, Vue, Angular, Blazor Server, ASP.NET Core, Web Forms, Classic ASP, PHP bindings.

Why teams move

  • No webpack / custom build. CKEditor 5 requires a build pipeline to mix features. We ship one bundle and toggle features via config.
  • Perpetual license vs CKEditor’s five separate subscription SKUs (Real-time Collab, Track Changes, Comments, Revision History, AI).
  • Self-host on entry tier — CKEditor’s enterprise features are Custom-tier pricing.
  • Wider framework coverage — Blazor, Web Forms, Classic ASP, PHP, and more.

Step-by-step checklist

  1. Install the npm package: npm install @@richscripts2/richtexteditor
  2. Remove your CKEditor custom build / webpack config entirely.
  3. Replace ClassicEditor.create(...) with new RichTextEditor("#editor", { toolbar: "full" });
  4. Map your CKEditor toolbar: [...] items to our toolbar config (see cheat sheet).
  5. Collaboration: replace CKEditor Cloud Services with editor.collab.attach({ doc, provider, textSync: "crdt" }) and self-host Yjs + WebSocket provider. (Use textSync: true if you want the legacy snapshot mode for back-compat.)
  6. AI: swap cloudServices config for editor.aiToolkit.setResolver(fn).
  7. Paste existing CKEditor content directly — HTML output is compatible.

Config cheat sheet

Basic setup

CKEditor 5 (custom build)

import ClassicEditor from
  '@@ckeditor/ckeditor5-build-classic';

ClassicEditor.create(
  document.querySelector('#editor'),
  {
    toolbar: {
      items: ['bold','italic','|',
        'bulletedList','numberedList','|',
        'link','imageUpload']
    }
  }
);

RichTextEditor (no build step)

<link rel="stylesheet"
      href="richtexteditor/rte_theme_default.css">
<script src="richtexteditor/rte.js"></script>

<div id="editor"></div>

<script>
new RichTextEditor("#editor", {
  toolbar: "custom",
  toolbar_custom:
    "{bold,italic}|{insertunorderedlist,insertorderedlist}"
    + "|{insertlink,insertimage}"
});
</script>

Real-time collaboration

CKEditor Cloud Services (paid)

ClassicEditor.create(el, {
  cloudServices: {
    tokenUrl: '/api/cke-token',
    webSocketUrl:
      'wss://cs.cke-cs.com/...'
  },
  collaboration: { channelId: docId }
});

RichTextEditor + Yjs (self-host)

import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const doc = new Y.Doc();
const provider = new WebsocketProvider(
  "wss://your-server", docId, doc);

editor.collab.attach({
  doc, provider,
  user: { id, name, color },
  textSync: "crdt"  // per-node Yjs concurrent typing
});

AI Assistant

CKEditor AI (subscription + credits)

ClassicEditor.create(el, {
  ai: {
    openAI: {
      requestHeaders: { Authorization: '...' },
      apiUrl: 'https://api.openai.com/v1/...'
    }
  }
});

RichTextEditor (any provider)

var editor = new RichTextEditor("#editor", {
  aiToolkitEnabled: true
});

editor.aiToolkit.setResolver(async (req) => {
  const r = await fetch("/api/my-ai", {
    method: "POST",
    body: JSON.stringify(req)
  });
  return r.json();
});

Plugin equivalence

CKEditor 5 featureRichTextEditor equivalent
Bold, Italic, Underlinebuilt-in (bold, italic, underline)
Link / LinkImagebuilt-in (insertlink)
Image / ImageUpload / ImageResizebuilt-in (insertimage, imageeditor)
Table / TablePropertiesbuilt-in (inserttable + table control toolbars)
List / TodoListbuilt-in (insertorderedlist, insertunorderedlist, insertchecklist)
CodeBlockbuilt-in (insertcode)
Mention (premium)mentionEnabled: true (base)
Comments (premium)commentsEnabled: true (base)
TrackChanges (premium)trackChangesEnabled: true (base)
RevisionHistory (premium)revisionHistoryEnabled: true (base)
RealTimeCollaborativeEditing (premium)editor.collab.attach({ textSync: "crdt" }) + self-host Yjs (per-node CRDT, base license)
AIAssistant (premium + usage)aiToolkitEnabled: true + setResolver(fn)
ExportWord (premium Cloud Services)editor.aiToolkit.exportDocx() helper
ExportPdf (premium)html2pdf toolbar item (client-side)
Autosaverevision history auto-snapshots
PasteFromOfficebuilt-in (paste-from-Word with improved list fidelity)

Behavior differences to know about

Content model. CKEditor 5 uses a custom model tree and serializes to HTML. RichTextEditor is HTML-native via contenteditable. For most apps the difference is invisible. If you have a custom schema, our structured-content JSON mode (enableStructuredContent: true) is the closest equivalent.
Real-time collaboration. CKEditor’s cloud-hosted OT delivers concurrent-typing with zero setup (monthly fee per Cloud Services). Our textSync: "crdt" mode ships per-node Yjs CRDT in the base license and pairs with any Yjs provider — self-host y-websocket or Hocuspocus. One-time license vs. ongoing subscription.
Custom builds. CKEditor 5’s webpack dance is gone — we ship one bundle with every feature, toggled via config flags. Delete your ckeditor5-inspector and build tooling.

What you gain

  • No build pipeline — one <script> tag, works.
  • Perpetual license vs five separate CKEditor subscriptions.
  • Self-host everything day one — including Yjs collab (your choice of provider).
  • Dictation, mobile toolbar mode, and public release notes out of the box.
  • Blazor Server, ASP.NET Web Forms, Classic ASP, PHP bindings.

Why teams move

  • Lower cost. Froala’s commercial license is per-project. We’re a perpetual domain license that includes Track Changes, Comments, Revision History, and the AI Toolkit — features Froala doesn’t ship at all.
  • Wider feature set. Froala has no Track Changes and no Threaded Comments. Our base license ships both in the base license.
  • BYOK AI without manual integration. Froala AI Assist requires hand-rolling the resolver call; ours is one-line setResolver(fn) on the client or DI on the server.
  • No webpack. Drop a single <script> tag and you’re done; Froala’s plugin selection requires careful module imports.

Step-by-step checklist

  1. Install the npm package: npm install @@richscripts2/richtexteditor
  2. Replace your Froala script tags with <script src="richtexteditor/rte.js"></script> + <link rel="stylesheet" href="richtexteditor/rte_theme_default.css">.
  3. Replace new FroalaEditor('#editor', { ... }) with new RichTextEditor("#editor", { toolbar: "full" });
  4. Map your toolbarButtons array to a toolbar_* config string (see cheat sheet below).
  5. If you use Froala AI Assist, swap the manual fetch / register-command for editor.aiToolkit.setResolver(async (req) => ...).
  6. Existing HTML keeps working; submit forms / read content as normal.

Config cheat sheet

Basic setup

Froala

new FroalaEditor('#editor', {
  toolbarButtons: [
    'bold', 'italic', 'underline', '|',
    'formatOL', 'formatUL', '|',
    'insertLink', 'insertImage'
  ],
  height: 500
});

RichTextEditor

new RichTextEditor("#editor", {
  toolbar: "custom",
  toolbar_custom:
    "{bold,italic,underline}|{insertorderedlist,insertunorderedlist}"
    + "|{insertlink,insertimage}",
  height: "500px"
});

AI integration

Froala AI Assist (v5.1+)

FroalaEditor.RegisterCommand('aiAssist', {
  callback: function () {
    fetch('/my-proxy', { ... })
      .then(/* manual integration */);
  }
});

RichTextEditor (one-liner)

var editor = new RichTextEditor("#editor", {
  aiToolkitEnabled: true
});

editor.aiToolkit.setResolver(async (req) => {
  const r = await fetch("/api/my-ai", {
    method: "POST", body: JSON.stringify(req)
  });
  return r.json();
});

Plugin equivalence

Froala plugin / buttonRichTextEditor equivalent
bold, italic, underlinebuilt-in (bold, italic, underline)
fontFamily, fontSizebuilt-in (fontname, fontsize)
colorbuilt-in (forecolor, backcolor)
formatOL, formatULbuilt-in (insertorderedlist, insertunorderedlist)
insertLink, insertImage, insertVideo, insertTablebuilt-in
emoticons / specialCharactersbuilt-in (insertemoji, insertchars)
codeViewbuilt-in (code)
fullscreenbuilt-in (fullscreenenter, fullscreenexit)
printbuilt-in (print)
spellCheckerbrowser-native + spellcheck toolbar toggle
Track ChangestrackChangesEnabled: trueFroala doesn’t ship this
CommentscommentsEnabled: trueFroala doesn’t ship this
Revision HistoryrevisionHistoryEnabled: true — Froala has only basic undo
AI Assist (5.1+)aiToolkitEnabled: true + resolver
Word exporteditor.aiToolkit.exportDocx() helper (when server endpoint is wired)
PDF exporthtml2pdf toolbar item (client-side)

Behavior differences to know about

HTML output. Froala adds framework-specific attributes (class="fr-fic", etc.) for image alignment. Our output uses standard inline styles — cleaner for downstream consumers.
Events. Froala’s events: { 'contentChanged': fn }editor.attachEvent("change", fn).
iframe vs. div. Froala edits in a contenteditable div. We use an iframe by default for content isolation; pass config.iframe = false to match Froala’s in-page behaviour if your CSS depends on it.

What you gain

  • Track Changes + Threaded Comments + Revision History — Froala ships none of these.
  • Slash commands & @@mentions — Notion-style inline picker.
  • Yjs real-time presence + concurrent typing preview.
  • Dictation via Web Speech API.
  • React, Vue, Angular, Blazor Server, ASP.NET Core, Web Forms, Classic ASP, PHP bindings.

Why teams move

  • Quill is feature-frozen. Quill 2.0 (April 2024) restored maintenance but the module ecosystem stayed thin. No first-party Track Changes, Comments, Revisions, AI, Mentions, or Dictation — community modules vary in quality and abandonment risk.
  • Delta ↔ HTML round-trip is lossy. Quill stores content as a Delta JSON document; consumers downstream usually want HTML or DOCX. We give you HTML, structured JSON, Markdown, and DOCX export from one API.
  • Custom blots & formats are heavy work. Adding a new content type in Quill means writing a Blot subclass and registering it. Our toolbar / dropdown / plugin factories are a few lines.
  • Frameworks aren’t first-class. Quill has community React / Vue wrappers; we ship native React, Vue, Angular, Blazor Server, ASP.NET Web Forms, Classic ASP, and PHP bindings.

Step-by-step checklist

  1. Install: npm install @@richscripts2/richtexteditor
  2. Replace your Quill assets: drop quill.snow.css and quill.js; add <link rel="stylesheet" href="richtexteditor/rte_theme_default.css"> + <script src="richtexteditor/rte.js"></script>.
  3. Replace new Quill('#editor', { ... }) with new RichTextEditor("#editor", { toolbar: "full" });
  4. Convert any existing Delta documents to HTML once: pass them through Quill's QuillDeltaToHtmlConverter (or your own renderer) and store the HTML. After that, content is plain HTML.
  5. Replace Delta read / write paths with editor.getHTMLCode() / editor.setHTMLCode(html) — or getJSON() / setJSON() if you want to keep a structured-content workflow.
  6. Map your modules.toolbar array to a toolbar string (cheat sheet below).
  7. If you have custom Blots (mention chips, embeds, etc.) replace them with our mentions / slashCommands APIs or with insertElement().

Config cheat sheet

Basic setup

Quill 2.x

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    toolbar: [
      ['bold', 'italic', 'underline'],
      [{ list: 'ordered' }, { list: 'bullet' }],
      ['link', 'image'],
      ['clean']
    ]
  }
});

const delta = quill.getContents();
const html  = quill.root.innerHTML;

RichTextEditor

const editor = new RichTextEditor("#editor", {
  toolbar: "custom",
  toolbar_custom:
    "{bold,italic,underline}|" +
    "{insertorderedlist,insertunorderedlist}|" +
    "{insertlink,insertimage}|" +
    "{removeformat}"
});

const html = editor.getHTMLCode();
const json = editor.getJSON(); // structured-content

Mentions (Quill needs quill-mention module)

Quill + quill-mention

// install: npm i quill-mention
import 'quill-mention';

new Quill('#editor', {
  modules: {
    mention: {
      allowedChars: /^[A-Za-z\s]*$/,
      mentionDenotationChars: ['@@', '#'],
      source: function (search, render) {
        fetch('/api/users?q=' + search)
          .then(r => r.json())
          .then(render);
      }
    }
  }
});

RichTextEditor (built-in)

const editor = new RichTextEditor("#editor", {
  mentionsEnabled: true
});

editor.mentions.register({
  trigger: "@@",
  search: async (q) => {
    const r = await fetch("/api/users?q=" + q);
    return r.json(); // [{ id, name }]
  }
});

Module / API equivalence

Quill module / patternRichTextEditor equivalent
quill.getContents() (Delta)editor.getJSON() (structured) or editor.getHTMLCode() (HTML)
quill.setContents(delta)editor.setJSON(value) or editor.setHTMLCode(html)
quill.on('text-change', fn)editor.attachEvent("change", fn)
quill.on('selection-change', fn)editor.attachEvent("selectionchange", fn)
quill.format('bold', true)editor.execCommand("bold")
quill.formatText(...) custom Bloteditor.surroundByTagName(tag) or editor.insertElement(node)
quill-mention modulebuilt-in editor.mentions.register()
quill-image-resize community modulebuilt-in image resize handles
quill-table community modulebuilt-in table commands (insert / row / col / merge)
Track Changesbuilt-in trackChangesEnabled: trueQuill has none
Threaded commentsbuilt-in commentsEnabled: trueQuill has none
Revision historybuilt-in revisionHistoryEnabled: trueQuill has none
AI / Ask AI / Chat / Reviewbuilt-in AI Toolkit — Quill has none
DOCX / PDF exportbuilt-in aiToolkit.exportDocx(), html2pdfQuill has none
Yjs real-time collabbuilt-in editor.collab.attach() — Quill via y-quill third-party

Behavior differences to know about

Document model. Quill is Delta-first. We’re HTML-first with optional structured-content JSON. Run a one-time Delta→HTML migration on existing content (we provide a migration helper) and you never touch Delta again.
Themes. Quill’s snow / bubble themes → our themed CSS plus config.toolbar presets ("basic", "full", "mobile", "custom").
Custom Blots. If your codebase has many custom Blots, plan a few hours per blot to port them. Most reduce to one-line plugins or a createToolbarButton() + insertElement() pair.

What you gain

  • Comments + Track Changes + Revision History — all base license; Quill has none.
  • AI Toolkit with Ask AI, AI Chat, AI Review drawer, BYOK resolver, streaming, DOCX export.
  • HTML / JSON / Markdown all from one editor — no Delta converter library.
  • Slash commands & @@mentions built in, no third-party module risk.
  • Frameworks beyond JS — Blazor Server, ASP.NET Core / Web Forms / Classic ASP, PHP.

Why teams move

  • Lexical is a framework, not an editor. Meta’s headless model means you build every toolbar, every plugin, every UI piece yourself. Productive teams ship in weeks; the productivity tax never goes away. We ship a finished editor and let you customize on top.
  • No commercial features out of the box. Lexical is MIT and has no track changes, comments, revisions, AI, dictation, DOCX export, or PDF export in core. You either build them or pull in community / commercial add-ons (Liveblocks, Mantle, etc.).
  • No commercial support tier. Lexical’s GitHub is the only support channel. We answer support tickets directly, ship hotfixes, and you have a vendor to call.
  • React-locked. Lexical’s ergonomic surface is the React package; non-React frameworks reach into @@lexical/headless with a steep learning curve. We ship native bindings for 7 frameworks.

Step-by-step checklist

  1. Install: npm install @@richscripts2/richtexteditor
  2. Decide: keep Lexical’s rendered output as initial content, or have users re-edit it inline. Either path works because Lexical serializes to HTML on export ($generateHtmlFromNodes).
  3. Replace <LexicalComposer> + <RichTextPlugin> + every plugin component with a single <RichTextEditorComponent> from @@richscripts2/richtexteditor/react.
  4. Delete your custom toolbar component, OnChangePlugin, HistoryPlugin, ListPlugin, LinkPlugin, MarkdownShortcutPlugin — all built in.
  5. If you have custom Lexical nodes (mention chips, embeds), port them to editor.mentions.register() / editor.slashCommands.register() / editor.insertElement().
  6. If you used Liveblocks for real-time, swap to editor.collab.attach({ doc, provider }) with a Yjs provider.
  7. Replace your custom DOCX export pipeline with editor.aiToolkit.exportDocx().

Config cheat sheet

Basic setup

Lexical (React)

// You build all of this yourself:
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 { ListPlugin } from '@@lexical/react/LexicalListPlugin';
import { LinkPlugin } from '@@lexical/react/LexicalLinkPlugin';
import ToolbarPlugin from './ToolbarPlugin'; // ← ~300 LOC of your own code

<LexicalComposer initialConfig={config}>
  <ToolbarPlugin />
  <RichTextPlugin
    contentEditable={<ContentEditable />}
    placeholder={<div>Enter…</div>}
    ErrorBoundary={LexicalErrorBoundary}
  />
  <HistoryPlugin />
  <ListPlugin /> <LinkPlugin />
  <OnChangePlugin onChange={onChange} />
</LexicalComposer>

RichTextEditor (React)

import { RichTextEditorComponent } from
  "@@richscripts2/richtexteditor/react";

<RichTextEditorComponent
  config={{
    toolbar: "full",
    enableAiToolkit: true,
    commentsEnabled: true,
    revisionHistoryEnabled: true
  }}
  onChange={(html) => setHtml(html)}
/>

Custom node → custom button

Lexical custom node

// 1. extend DecoratorNode
class CalloutNode extends DecoratorNode { ... }
// 2. importJSON / exportJSON
// 3. createDOM / updateDOM
// 4. transform plugin to insert it
// 5. toolbar button to call $insertNodes
// 6. theme entry
// 7. SSR rendering helper
// (each step is real code)

RichTextEditor custom button

editor.createToolbarButton("callout", {
  text: "Callout",
  click: (ed) => {
    const div = document.createElement("div");
    div.className = "callout";
    div.innerHTML = ed.getSelectedText();
    ed.insertElement(div);
  }
});

Lexical concept → RichTextEditor equivalent

LexicalRichTextEditor
editor.getEditorState() + $generateHtmlFromNodeseditor.getHTMLCode()
$getRoot() + manual JSONeditor.getJSON()
$insertNodes(...) inside editor.update(...)editor.insertHTML(html) / editor.insertElement(node)
OnChangePlugineditor.attachEvent("change", fn)
HistoryPlugin (custom undo)built-in undo / redo — nothing to wire
ListPlugin, LinkPlugin, MarkdownShortcutPluginbuilt-in
Custom DecoratorNodecreateToolbarButton() + insertElement()
@@lexical/mark for highlightsbuilt-in highlight / backcolor
Liveblocks / Yjs binding (DIY)built-in editor.collab.attach()
Comments (community / paid Liveblocks)built-in commentsEnabled: true
Track changes (none in core)built-in trackChangesEnabled: true
AI integration (DIY)built-in AI Toolkit (setResolver / streaming / DOCX)

Behavior differences to know about

Headless → finished. Lexical needs you to wire toolbar, history, lists, links, undo, paste handling, table support, image resize, and theme. We ship all of it. Expect to delete more code than you write during the migration.
EditorState → HTML. Lexical’s native format is EditorState JSON. If your backend stores Lexical state, run a one-time export to HTML using $generateHtmlFromNodes and store HTML afterwards.
Decorator nodes are React components. If you have inline React UI inside content (e.g. interactive embeds), wrap them in standard DOM elements with data-attributes; we hydrate them via insertElement() + your own React mount logic in onChange.

What you gain

  • Finished editor — toolbar, dialogs, paste filtering, table support, image editor, all out of the box.
  • Commercial support with named contacts, hotfixes, and a roadmap.
  • AI Toolkit, Track Changes, Comments, Revision History, Mentions, Slash Commands, Dictation — all base license.
  • Frameworks beyond React — Vue, Angular, Blazor Server, ASP.NET Core / Web Forms / Classic ASP, PHP.
  • DOCX + PDF export — Lexical has neither.

Why teams move

  • Pricing. Tiptap’s premium extensions (Comments, History, Document AI, Mathematics, Drag handle) and Tiptap Cloud (real-time, document storage) are paid per-author or per-doc subscriptions. We’re a one-time perpetual license that includes everything.
  • Headless tax. Tiptap is ProseMirror under the hood — powerful, but you build your toolbar, dialogs, paste behaviour, table UI, and image-edit affordances yourself. We ship a finished editor.
  • One bundle. Tiptap is dozens of npm packages (@@tiptap/core, @@tiptap/starter-kit, @@tiptap/extension-*); we ship one rte.js with feature flags.
  • Self-host without an enterprise contract. Tiptap Cloud’s real-time and Document AI features require their hosted services or an Enterprise self-host agreement. Our collab + AI ship in the base download.

Step-by-step checklist

  1. Install: npm install @@richscripts2/richtexteditor
  2. Drop your @@tiptap/* packages and the Tiptap Pro packages from package.json.
  3. Replace useEditor({ extensions, content }) + <EditorContent /> with <RichTextEditorComponent config={...} /> from @@richscripts2/richtexteditor/react (or the Vue / Angular / vanilla variants).
  4. Delete your custom toolbar React component — built in via config.toolbar.
  5. If you used @@tiptap/extension-collaboration + y-prosemirror, swap for editor.collab.attach({ doc, provider }). The Y.Doc and provider stay yours.
  6. If you used Tiptap Pro Comments / Document History / Document AI, replace them with the base-license equivalents (commentsEnabled, revisionHistoryEnabled, aiToolkitEnabled + setResolver).
  7. Custom Tiptap extensions (Mark / Node) port to createToolbarButton() + insertElement() / surroundByTagName().

Config cheat sheet

Basic setup

Tiptap (React)

import { useEditor, EditorContent } from '@@tiptap/react';
import StarterKit from '@@tiptap/starter-kit';
import Link from '@@tiptap/extension-link';
import Image from '@@tiptap/extension-image';
import Placeholder from '@@tiptap/extension-placeholder';

const editor = useEditor({
  extensions: [
    StarterKit, Link, Image,
    Placeholder.configure({ placeholder: 'Type…' })
  ],
  content: '<p>Hello</p>',
  onUpdate: ({ editor }) => setHtml(editor.getHTML())
});

<MyToolbar editor={editor} />     // ~150 LOC you wrote
<EditorContent editor={editor} />

RichTextEditor (React)

import { RichTextEditorComponent } from
  "@@richscripts2/richtexteditor/react";

<RichTextEditorComponent
  config={{
    toolbar: "full",
    placeholder: "Type…",
    enableAiToolkit: true,
    commentsEnabled: true,
    revisionHistoryEnabled: true
  }}
  onChange={setHtml}
/>

Real-time collab

Tiptap Pro Collab

import Collaboration from '@@tiptap/extension-collaboration';
import CollaborationCursor from '@@tiptap/extension-collaboration-cursor';
import * as Y from 'yjs';
import { TiptapCollabProvider } from '@@hocuspocus/provider';
// ↑ Tiptap Cloud subscription OR self-hosted Hocuspocus

const ydoc = new Y.Doc();
const provider = new TiptapCollabProvider({
  appId: 'XXXXX',
  name: 'doc-1', document: ydoc, token: '...'
});

useEditor({
  extensions: [
    StarterKit.configure({ history: false }),
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({ provider, user })
  ]
});

RichTextEditor (no add-on)

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
  'wss://your-server', 'doc-1', ydoc
);

editor.collab.attach({
  doc: ydoc,
  provider,
  textSync: "crdt"  // per-node Yjs concurrent typing
});

AI

Tiptap Document AI (Pro)

// Subscription required.
import Ai from '@@tiptap-pro/extension-ai';

useEditor({
  extensions: [
    StarterKit,
    Ai.configure({
      appId: 'XXXXX',
      token: '...',
      autocompletion: true
    })
  ]
});

RichTextEditor AI Toolkit (base license)

const editor = new RichTextEditor("#editor", {
  enableAiToolkit: true
});

editor.aiToolkit.setResolver(async (req) => {
  const r = await fetch("/api/ai", {
    method: "POST",
    body: JSON.stringify(req)
  });
  return r.json(); // any provider; BYOK
});

Extension → built-in mapping

Tiptap extensionRichTextEditor equivalent
StarterKit (paragraph, heading, bold, italic, lists, blockquote, code, history)built-in toolbar (toolbar: "full" or "basic")
@@tiptap/extension-linkbuilt-in (insertlink, unlink)
@@tiptap/extension-imagebuilt-in (insertimage) + image-edit dialog
@@tiptap/extension-tablebuilt-in (insert / row / col / merge / split)
@@tiptap/extension-placeholderconfig.placeholder
@@tiptap/extension-mentionbuilt-in editor.mentions.register()
@@tiptap/extension-task-list + extension-task-itembuilt-in checklist
@@tiptap/extension-collaboration + providerbuilt-in editor.collab.attach() with any Yjs provider
@@tiptap-pro/extension-comments (paid)built-in, base license commentsEnabled: true
@@tiptap-pro/extension-history (paid)built-in, base license revisionHistoryEnabled: true
@@tiptap-pro/extension-ai (paid)built-in, base license AI Toolkit + setResolver(fn)
@@tiptap-pro/extension-mathematics (paid)third-party path: integrate WIRIS or MathLive via createToolbarButton() + insertElement()
@@tiptap-pro/extension-drag-handle (paid)built-in block drag (paragraph drag handle on hover)
Tiptap Cloud Document Storagenone required — you keep storage; we’re client-side + DI server endpoint

Behavior differences to know about

One bundle vs. dozens. Tiptap forces tree-shaking decisions per extension. We’re one bundle with feature flags — easier ops, slightly larger initial download. For most apps the diff is negligible after gzip.
HTML output. Tiptap’s output is ProseMirror-clean HTML (no &nbsp; in empty paragraphs, semantic tags). Ours is HTML-native with the same hygiene. Existing content moves over without changes.
Concurrent typing CRDT. Tiptap Pro’s Hocuspocus gives per-character co-authoring (paid). Our textSync: "crdt" mode delivers the same per-node Yjs CRDT in the base license and works with any Yjs provider — self-host y-websocket or Hocuspocus. Awareness + cursor sharing also work today.
Custom extensions. Tiptap extensions are ProseMirror plugins. Most port to a createToolbarButton() + insertElement(); complex schema-level extensions may need a one-off discussion — talk to us.

What you gain

  • Comments + Track Changes + Revision History + AI all in the base license — no Pro subscription, no Cloud account.
  • Finished editor — no toolbar component to write, no paste-handling gotchas.
  • BYOK AI with any provider — no Tiptap-managed token, no per-author seat metering.
  • One-time perpetual license instead of per-author / per-doc subscriptions.
  • Frameworks beyond React / Vue — Angular, Blazor Server, ASP.NET Core / Web Forms / Classic ASP, PHP.

Ready to switch?

Download the trial, run it on your project, and see the migration in practice.

Need help with migration? support@@richtexteditor.com