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
- Install the npm package:
npm install @@richscripts2/richtexteditor
- Replace your TinyMCE
<script src="tinymce.min.js"> with <link rel="stylesheet" href="richtexteditor/rte_theme_default.css"> + <script src="richtexteditor/rte.js">.
- Remove the TinyMCE init script (
tinymce.init({ selector: ... })) and replace the <textarea> with <div id="editor">.
- Instantiate:
new RichTextEditor("#editor", { toolbar: "full" });
- Map your TinyMCE
plugins: and toolbar: to our toolbar config — see the cheat sheet below.
- Wire your AI resolver via
editor.aiToolkit.setResolver(async (req) => { ... }). No proxy needed.
- 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 plugin | RichTextEditor equivalent |
link | built-in (insertlink) |
image, imagetools | built-in (insertimage, imageeditor) |
media | built-in (insertvideo, insertyoutube) |
table | built-in (inserttable) |
lists, advlist | built-in (insertorderedlist, insertunorderedlist, insertchecklist) |
codesample | built-in (insertcode, syntaxhighlighter) |
emoticons | built-in (insertemoji) |
template | built-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
- Install the npm package:
npm install @@richscripts2/richtexteditor
- Remove your CKEditor custom build / webpack config entirely.
- Replace
ClassicEditor.create(...) with new RichTextEditor("#editor", { toolbar: "full" });
- Map your CKEditor
toolbar: [...] items to our toolbar config (see cheat sheet).
- 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.)
- AI: swap
cloudServices config for editor.aiToolkit.setResolver(fn).
- 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 feature | RichTextEditor equivalent |
Bold, Italic, Underline | built-in (bold, italic, underline) |
Link / LinkImage | built-in (insertlink) |
Image / ImageUpload / ImageResize | built-in (insertimage, imageeditor) |
Table / TableProperties | built-in (inserttable + table control toolbars) |
List / TodoList | built-in (insertorderedlist, insertunorderedlist, insertchecklist) |
CodeBlock | built-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) |
Autosave | revision history auto-snapshots |
PasteFromOffice | built-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
- Install the npm package:
npm install @@richscripts2/richtexteditor
- Replace your Froala script tags with
<script src="richtexteditor/rte.js"></script> + <link rel="stylesheet" href="richtexteditor/rte_theme_default.css">.
- Replace
new FroalaEditor('#editor', { ... }) with new RichTextEditor("#editor", { toolbar: "full" });
- Map your
toolbarButtons array to a toolbar_* config string (see cheat sheet below).
- If you use Froala AI Assist, swap the manual fetch / register-command for
editor.aiToolkit.setResolver(async (req) => ...).
- 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 / button | RichTextEditor equivalent |
bold, italic, underline | built-in (bold, italic, underline) |
fontFamily, fontSize | built-in (fontname, fontsize) |
color | built-in (forecolor, backcolor) |
formatOL, formatUL | built-in (insertorderedlist, insertunorderedlist) |
insertLink, insertImage, insertVideo, insertTable | built-in |
emoticons / specialCharacters | built-in (insertemoji, insertchars) |
codeView | built-in (code) |
fullscreen | built-in (fullscreenenter, fullscreenexit) |
print | built-in (print) |
spellChecker | browser-native + spellcheck toolbar toggle |
| Track Changes | trackChangesEnabled: true — Froala doesn’t ship this |
| Comments | commentsEnabled: true — Froala doesn’t ship this |
| Revision History | revisionHistoryEnabled: true — Froala has only basic undo |
| AI Assist (5.1+) | aiToolkitEnabled: true + resolver |
| Word export | editor.aiToolkit.exportDocx() helper (when server endpoint is wired) |
| PDF export | html2pdf 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
- Install:
npm install @@richscripts2/richtexteditor
- 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>.
- Replace
new Quill('#editor', { ... }) with new RichTextEditor("#editor", { toolbar: "full" });
- 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.
- Replace Delta read / write paths with
editor.getHTMLCode() / editor.setHTMLCode(html) — or getJSON() / setJSON() if you want to keep a structured-content workflow.
- Map your
modules.toolbar array to a toolbar string (cheat sheet below).
- 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 / pattern | RichTextEditor 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 Blot | editor.surroundByTagName(tag) or editor.insertElement(node) |
quill-mention module | built-in editor.mentions.register() |
quill-image-resize community module | built-in image resize handles |
quill-table community module | built-in table commands (insert / row / col / merge) |
| Track Changes | built-in trackChangesEnabled: true — Quill has none |
| Threaded comments | built-in commentsEnabled: true — Quill has none |
| Revision history | built-in revisionHistoryEnabled: true — Quill has none |
| AI / Ask AI / Chat / Review | built-in AI Toolkit — Quill has none |
| DOCX / PDF export | built-in aiToolkit.exportDocx(), html2pdf — Quill has none |
| Yjs real-time collab | built-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
- Install:
npm install @@richscripts2/richtexteditor
- 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).
- Replace
<LexicalComposer> + <RichTextPlugin> + every plugin component with a single <RichTextEditorComponent> from @@richscripts2/richtexteditor/react.
- Delete your custom toolbar component, OnChangePlugin, HistoryPlugin, ListPlugin, LinkPlugin, MarkdownShortcutPlugin — all built in.
- If you have custom Lexical nodes (mention chips, embeds), port them to
editor.mentions.register() / editor.slashCommands.register() / editor.insertElement().
- If you used Liveblocks for real-time, swap to
editor.collab.attach({ doc, provider }) with a Yjs provider.
- 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
| Lexical | RichTextEditor |
editor.getEditorState() + $generateHtmlFromNodes | editor.getHTMLCode() |
$getRoot() + manual JSON | editor.getJSON() |
$insertNodes(...) inside editor.update(...) | editor.insertHTML(html) / editor.insertElement(node) |
OnChangePlugin | editor.attachEvent("change", fn) |
HistoryPlugin (custom undo) | built-in undo / redo — nothing to wire |
ListPlugin, LinkPlugin, MarkdownShortcutPlugin | built-in |
Custom DecoratorNode | createToolbarButton() + insertElement() |
@@lexical/mark for highlights | built-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
- Install:
npm install @@richscripts2/richtexteditor
- Drop your
@@tiptap/* packages and the Tiptap Pro packages from package.json.
- Replace
useEditor({ extensions, content }) + <EditorContent /> with <RichTextEditorComponent config={...} /> from @@richscripts2/richtexteditor/react (or the Vue / Angular / vanilla variants).
- Delete your custom toolbar React component — built in via
config.toolbar.
- If you used
@@tiptap/extension-collaboration + y-prosemirror, swap for editor.collab.attach({ doc, provider }). The Y.Doc and provider stay yours.
- If you used Tiptap Pro Comments / Document History / Document AI, replace them with the base-license equivalents (
commentsEnabled, revisionHistoryEnabled, aiToolkitEnabled + setResolver).
- 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 extension | RichTextEditor equivalent |
StarterKit (paragraph, heading, bold, italic, lists, blockquote, code, history) | built-in toolbar (toolbar: "full" or "basic") |
@@tiptap/extension-link | built-in (insertlink, unlink) |
@@tiptap/extension-image | built-in (insertimage) + image-edit dialog |
@@tiptap/extension-table | built-in (insert / row / col / merge / split) |
@@tiptap/extension-placeholder | config.placeholder |
@@tiptap/extension-mention | built-in editor.mentions.register() |
@@tiptap/extension-task-list + extension-task-item | built-in checklist |
@@tiptap/extension-collaboration + provider | built-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 Storage | none 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 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