Days
Hours
Minutes
Seconds
x
Skip to content

Make Your AI Coding Agent a Froala Expert

Froala agent skills

Meet Froala Agent Skills, an open set of skills that gives your AI coding agent accurate, up-to-date knowledge of Froala.

If you’ve asked an AI coding agent to help with Froala, you’ve probably seen it happily write code that looks right but isn’t. Some mistakes fail silently: a toolbar button never shows up, or your production site displays an unlicensed banner. Others are simply out of date, like Froala’s old jQuery-style initializer. Either way, you lose hours to debugging, and your app looks unfinished to your users. These skills fix that by teaching your agent how Froala works today.

For example, your AI coding agent may respond with something like this:

JavaScript
// What an AI agent often writes from older training data
$('#editor').froalaEditor({
  licenseKey: 'YOUR_KEY',
  toolbarButtons: ['bold', 'italic', 'insertImage'],
  events: {
    'froalaEditor.contentChanged': (e, editor) => {
      console.log(editor.html.get());
    }
  }
});

It looks reasonable. It isn’t. The jQuery-style initializer and the froalaEditor. event prefix belong to Froala v2; since v3, Froala has been framework-free and uses new FroalaEditor(). The license option is key, not licenseKey, and Froala silently ignores unknown options, so the unlicensed banner stays on your production site with no error to tell you why. And unless the image plugin is loaded, the insertImage button simply never appears.

Here’s what the same request produces with Froala Agent Skills installed:

JavaScript
import FroalaEditor from 'froala-editor';
import 'froala-editor/css/froala_editor.pkgd.min.css';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/js/plugins/image.min.js';

new FroalaEditor('#editor', {
  key: process.env.FROALA_ACTIVATION_KEY, // never hardcode a production key
  toolbarButtons: ['bold', 'italic', 'insertImage'],
  events: {
    contentChanged: function () {
      console.log(this.html.get()); // `this` is the editor
    }
  }
});

No jQuery, the right license option read from an environment variable, both required stylesheets, the plugin the image button depends on, and a regular function so this points to the editor. Same prompt, working code.

Key takeaways

  • AI coding agents often write outdated or silently broken Froala code.
  • Froala Agent Skills is a set of seven open skills that give your agent accurate Froala knowledge, and they switch on automatically.
  • One command installs them: npx skills add froala/froala-skills.
  • They work in Claude Code, Cursor, Codex, GitHub Copilot and any agent that supports the SKILL.md format.

What’s inside

Froala Agent Skills is a collection of seven skills written in the open SKILL.md format, so they work in Claude Code, Cursor, Codex, GitHub Copilot and any other agent that supports it. Each skill packages what our team knows about one part of Froala: the right API names, the setup steps that are easy to miss, and the mistakes we see developers (and AI agents) make most often.

The skills are organized into four groups:

Group Skills What they cover
Integration froala-initialization-and-sdks, froala-filestack-integration Installing Froala in Vanilla JS, React, Vue, Angular and Next.js, licensing, and wiring Filestack for image and file uploads
Development froala-methods-and-events, froala-custom-plugins Events, getting and setting content, paste cleanup, custom toolbar buttons, dropdowns and popups
AI and documents froala-ai-assist, froala-document-features AI Assist and AI Chat, Word import and export, Markdown, Track Changes and real-time collaboration
Troubleshooting froala-error-diagnosis Missing toolbar buttons, unstyled editors, silent upload failures and other problems that produce no error

You never have to call a skill by name. Your agent always sees a short description of each one, and when your request or your code matches, it loads the full instructions on its own. Import react-froala-wysiwyg, type aiAssistRequest, or ask why a button isn’t showing, and the right skill switches on.

Install in one line

From your project folder, run:

Bash
npx skills add froala/froala-skills

This installs the skills for every compatible agent it finds in your project. Add -g to install them globally for all your projects, or -s <skill-name> to pick only the skills you want.

If you use Claude Code, you can install from the plugin marketplace instead:

Bash
/plugin marketplace add froala/froala-skills
/plugin install froala@froala-skills

Check that it worked

Run npx skills list (add -g if you installed globally) to see the installed skills. Then ask your agent: “Which option sets the Froala license key?” With the skills loaded, it should answer key and warn that licenseKey is silently ignored.

That’s the whole setup. Now let’s see what changes in practice.

Demo 1: Set up Froala in React

Prompt: “Add a Froala editor to my React app and keep the HTML in state.”

The froala-initialization-and-sdks skill activates and the agent writes:

JSX
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';
import 'froala-editor/js/plugins.pkgd.min.js'; // registers the plugins the toolbar uses

const config = {
  key: import.meta.env.VITE_FROALA_KEY,
  heightMin: 200,
  toolbarButtons: ['bold', 'italic', 'underline', '|', 'insertLink', 'insertImage'],
};

export default function Editor() {
  const [html, setHtml] = useState('<p>Hello from React!</p>');

  return (
    <FroalaEditorComponent
      tag="textarea"
      config={config}
      model={html}
      onModelChange={setHtml}
    />
  );
}

A few details the skill gets right that agents often miss:

  • Both stylesheets. froala_editor.pkgd.min.css styles the editor itself and froala_style.min.css styles the content. Leave one out and the editor renders but looks broken.
  • Plugins for every button. A toolbar button whose plugin isn’t loaded doesn’t throw an error; it just never renders. Importing plugins.pkgd.min.js covers the buttons above.
  • State, not manual reads. model and onModelChange keep your React state in sync. When you need to call editor methods directly, use a ref and ref.current.getEditor().
  • Next.js. On the App Router, 'use client' alone isn’t enough, because client components are still pre-rendered on the server. The agent loads the editor with next/dynamic and ssr: false to avoid the Element is not defined build error.

Demo 2: Turn on AI Assist

Prompt: “Add AI writing tools to the editor. Our backend already has an /api/ai endpoint.”

The froala-ai-assist skill activates:

JavaScript
new FroalaEditor('#editor', {
  aiSupplementalTermsAccepted: true,
  toolbarButtons: ['bold', 'italic', '|', 'aiAssist', 'aiShortCuts', 'aiChatAssistant'],

  aiAssistRequest: async function (data, signal) {
    const res = await fetch('/api/ai', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: data.prompt, context: data.context, model: data.model }),
      signal, // lets Froala cancel the request
    });
    const json = await res.json();
    return json.answer; // an HTML string
  },
});

This gives your users a free-form prompt panel, one-click shortcuts for changing tone, translating and improving writing, and an AI Chat panel. Here’s what the skill makes sure of:

  • The terms flag. Without aiSupplementalTermsAccepted: true, every AI button only opens a popup asking you to accept Froala’s AI Supplemental Terms, and nothing else happens. It’s the most common reason AI Assist “doesn’t work.” Review the terms before you set it.
  • Your keys stay on your server. Froala doesn’t ship a model. You connect your own provider (OpenAI, Google Gemini, Anthropic, Groq or your own service), and the skill routes requests through your backend with aiAssistRequest so provider API keys never reach the browser. Treat what comes back like any other user content: sanitize the HTML on your server before you store it.
  • HTML, not Markdown. Returning Markdown inserts it as plain text. The skill returns an HTML string.
  • Version gates. AI Assist needs Froala 5.1.0 or later, AI Chat needs 5.4.0 or later, and AI image generation needs 5.5.0 or later.

Demo 3: Fix a toolbar button that won’t show up

Prompt: “I added trackChanges to my toolbar, but the button doesn’t appear. There’s nothing in the console.”

This is the kind of bug that eats an afternoon, because Froala doesn’t throw an error when a button can’t render. The froala-error-diagnosis skill activates and the agent goes straight to the cause: the Track Changes plugin isn’t included in the pkgd bundles, so it has to be loaded on its own.

JavaScript
import 'froala-editor/js/plugins/track_changes.min.js';

new FroalaEditor('#editor', {
  toolbarButtons: ['bold', 'italic', '|', 'trackChanges', 'showChanges', 'applyAll', 'removeAll'],
});

If that isn’t the problem, the skill walks through the other usual suspects in order:

  • Plugin JS not imported. Buttons for images, links, tables and video need their plugin file loaded first.
  • pluginsEnabled is an allowlist. If you set it, any plugin you don’t list is turned off, even if you imported it.
  • Wrong button name. Names are case-sensitive: insertImage, formatOL, fullscreen.
  • Missing toolbar group. When toolbarButtons is a grouped object (moreText, moreParagraph, moreRich), a button that isn’t in any group is never rendered.

Frequently asked questions

Which AI coding agents does it work with?

Any agent that supports the open SKILL.md format, including Claude Code, Cursor, Codex and GitHub Copilot.

Do I need to call the skills myself?

No. Your agent reads a short description of each skill and loads the right one automatically when your request or code matches it.

Can I install only some of the skills?

Yes. Add -s <skill-name> to the install command to pick individual skills, for example npx skills add froala/froala-skills -s froala-ai-assist.

Do the skills send my code anywhere?

No. The skills are plain instruction files stored with your project. They don’t collect or send any data, and your agent handles your code exactly as it does today.

Is it free?

Yes. The skills are open source under the MIT license. You still need a Froala license to use the editor itself in production.

Try it, star it, and tell us what’s missing

AI coding agents are only as good as what they know. Froala Agent Skills gives them knowledge that comes straight from the team that builds the editor, so you spend less time correcting generated code and more time shipping.

Here’s how to get involved:

  1. Try it. Run npx skills add froala/froala-skills in a project, then paste this into your agent: “Add a Froala editor to this page with bold, italic and image buttons.” Compare the result with what you got before.
  2. Star the repo. Starring froala/froala-skills on GitHub helps other developers find it and tells us it’s worth investing in.
  3. Report issues. If a skill gives guidance that’s wrong, outdated or missing, open an issue. Include the prompt you used and what the agent produced; that’s the fastest way for us to fix it.

graphical user interface, text

Froala v5.5.0: AI Image Generation, Headless Mode, and More

Froala v5.5.0

We are excited to announce Froala v5.5.0, a release that brings:

  • AI image generation in AI Assist, so writers can create and insert visuals without leaving the editor.
  • Headless mode (initial release), which lets you run Froala with no built-in UI and build your own interface and styling on top of its editing engine.
  • Expanded Suggestion mode in the Collaboration plugin, making image, video, and link changes reviewable just like text edits.
  • More control and polish, including side panel state APIs, model-aware AI Chat toggles, new image and video resize events, and a round of bug fixes.

Here is what’s new and how to start using it.

Headless Mode (Initial Release)

What Is Froala Headless Mode?

A new headless option runs Froala as a pure editing engine. The editable area and all Action API methods (bold, align, lists, undo/redo, and more) keep working, but Froala draws none of its own UI:

  • Toolbar: never added to the page.
  • Popups, modals, and toasts: turned off. Methods that would open one simply do nothing.
  • Quick insert, line breaker, drag handles, image resize handles: not built.
  • Styling: the editor box gets an fr-headless class that removes Froala’s default font, color, padding, and border, so your own CSS decides how content looks.
  • Word paste: the keep/clean formatting modal is skipped and content is cleaned right away.
  • Character and word counters: off by default, but you can still turn them on.
  • Collaboration: disabled in headless mode, even if collabConfig is set.

Froala Headless mode

Benefits of Using Froala Headless Mode

Until now, there was no way to stop Froala from injecting its toolbar, popups, and styling. If your product has its own design system, you can now build a fully custom interface on top of Froala’s proven editing core, without fighting default UI or CSS.

A few methods were also updated so they work without a popup or toolbar click:

  • image.insertByURL(url) accepts the URL directly.
  • align.apply(), quote.apply(), and lists.format() focus the editor first if nothing is selected, so they always have a caret to work from.

How to Use Froala Headless Mode

const editor = new FroalaEditor('#editor', {
  headless: true
});

document.querySelector('#my-bold').addEventListener('click', function () {
  editor.commands.bold();
});

document.querySelector('#my-image').addEventListener('click', function () {
  editor.image.insertByURL('https://example.com/photo.jpg');
});

For plugin authors: five new helpers let your custom plugins behave correctly in headless mode, just like the built-in ones: 

  • helpers.isHeadless()

  • helpers.isUserOpt(key)

  • helpers.headlessDefault(key, value)

  • helpers.guardHeadlessUI(pluginApi, methodNames)

  • helpers.ensureHeadlessFocus().

FroalaEditor.PLUGINS.myHighlighter = function (editor) {
  function apply() {
    editor.helpers.ensureHeadlessFocus();
    editor.format.applyStyle('background-color', 'yellow');
  }
  return { apply: apply };
};

This is the first release of headless mode with an initial, limited feature scope. We plan to expand it in upcoming versions.

AI Image Generation in AI Assist

What it is

AI Assist is no longer limited to text. Users can describe the image they want in the AI Assist prompt, and Froala sends the request to the AI model you have configured. Then users can:

  • Preview the generated image inside the AI Assist popup.
  • Insert it at the cursor position with one click, using Froala’s standard image insertion, so the image behaves like any other image in the editor.
  • Regenerate it to get a new version from the same prompt.

The feature is provider-agnostic. It works with whichever AI service you connect, as long as that model supports image generation.

Froala AI image generation

Why it matters

Writers can illustrate blog posts, product pages, or docs in the same place they write, instead of switching to a separate image tool, downloading the file, and uploading it again. For you, there is no new UI to build: it reuses the AI Assist experience your users already know.

How to use it

Point AI Assist at your own backend, and have that backend call an image-capable model. When a user clicks Regenerate, Froala resends the original prompt and context with regenerate: true, so a caching backend knows to return a fresh image instead of the same one.

new FroalaEditor('.selector', {
  aiAssistRequest: async function (data, signal) {
    const response = await fetch('/api/ai', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        prompt: data.prompt,
        context: data.context,
        noCache: data.regenerate === true
      }),
      signal: signal
    });
    return response.json();
  }
});

Collaboration Upgrades

Suggestion Mode Now Covers Image, Video, and Link Edits

In the Collaboration plugin’s Suggestion mode, more edits now create a reviewable suggestion instead of applying instantly:

  • Resizing images and videos. Drag or keyboard resizes show up as an “Image resized” or “Video resized” card with a solid border around the element. Multiple resizes on the same element merge into one suggestion. Reject restores the size from before the first resize; Accept keeps the new size.
  • Image popup edits: replace (upload or URL), remove, alt text, size, display, alignment, styles, and captions.
  • Video popup edits: replace, remove, size, display, alignment, and autoplay.
  • Link popup edits: insert, edit (URL, text, target, rel, title), styles, and unlink.

Suggestion cards are also clearer. They now say what the edit did and replacements use their own distinct color.

Image resize

Why it matters: Reviewers no longer miss changes made through element popups. Every media and link change can be seen, discussed, and accepted or rejected like any text edit, which keeps team review workflows complete and trustworthy.

How to use it: No setup is needed. When Suggestion mode is on, these edits become suggestions automatically.

Full Control Over the Collaboration Side Panel

What it is: The comments and suggestions side panel now remembers the state the local user chose.

New API surface:

  • collabConfig.panel.defaultState: start the panel as 'expanded' (default), 'collapsed', or 'hidden'. It applies before the first paint, so there is no flash or layout shift on load.
  • collaborative.getPanelState(): read the current state.
  • collaborative.setPanelState(state): set the state from your own code.
  • collab.panelStateChanged event: fires with state, previousState, and origin whenever the state actually changes.

Why it matters: You can match the panel to your app’s layout from the very first render and build your own toggle buttons that always stay in sync.

How to use it:

const editor = new FroalaEditor('#editor', {
  collabConfig: {
    user: { id: 'user-1', name: 'Alex Walker', role: 'editor' },
    docId: 'my-document',
    panel: { defaultState: 'hidden' }
  },
  events: {
    'collab.panelStateChanged': function (state, previousState, origin) {
      document.querySelector('#show-comments')
        .classList.toggle('is-active', state !== 'hidden');
    }
  }
});

document.querySelector('#show-comments').addEventListener('click', function () {
  editor.collaborative.setPanelState('expanded');
});

AI Chat Respects Each Model’s Capabilities

What it is

The webSearch and reasoning flags on each aiChatModels entry now control the composer toggles. When a model sets a capability to false:

  • The toggle is disabled and cannot be turned on.
  • It is turned off automatically when the user switches to that model, with a short inline notice.
  • The flag is removed from requests sent for that model, including direct aiAssist.requestAnswer() calls.

Only an explicit false disables a toggle. Disabled toggles stay reachable by keyboard and screen readers, which announce why they are unavailable.

Froala AI Assist

Why it matters

Users can no longer turn on web search for a model that can’t search.

How to use it

new FroalaEditor('.selector', {
  aiChatModels: [
    {
      modelName: 'openai/gpt-oss-120b',
      displayName: 'GPT-OSS 120B',
      webSearch: true,
      reasoning: true
    },
    {
      modelName: 'llama-3.1-8b-instant',
      displayName: 'Llama 3.1 8B Instant',
      webSearch: false, // toggle is disabled for this model
      reasoning: false
    }
  ],
  aiChatWebSearchEnabled: true
});

New Image and Video Resize Events

What it is

New events now fire around every image and video resize, whether the user drags a handle or resizes with the keyboard:

  • image.resizeStart (new): fires when the user grabs a resize handle, before the size changes. It completes the existing image.resize and image.resizeEnd events.
  • video.resizeStart (new): fires before a video’s size changes.
  • video.resizeEnd (new): fires after the new video size is applied.

The replaced events also carry more information now:

  • image.replaced receives a third argument, old_src, with the replaced image’s source.
  • video.replaced receives the upload response (or undefined for URL/embed) and old_src.

Existing handlers keep working, since the original arguments are unchanged.

Why it matters

You can record an element’s size before and after a resize, which is useful for undo tools, audit logs, or saving layout changes to your backend. With old_src, you can also delete a replaced file from your server so unused uploads don’t pile up.

How to use it

new FroalaEditor('.selector', {
  events: {
    'image.resizeStart': function ($img) {
      console.log('Resizing from', $img[0].width, 'x', $img[0].height);
    },
    'video.resizeEnd': function ($video) {
      const $el = $video.find('iframe, video, embed');
      console.log('New size:', $el.attr('width'), 'x', $el.attr('height'));
    },
    'image.replaced': function ($img, response, old_src) {
      fetch('/images/delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ src: old_src })
      });
    }
  }
});

Additional Froala v5.5.0 Improvements and Fixes

Lists

  • New preserveListMarkerStyle option. By default (true), bolding or italicizing a whole list item styles the <li> so the bullet or number matches. Set it to false to keep semantic <strong> and <em> tags instead. This is useful when your HTML goes through a sanitizer that removes <li> styles, or into emails or Markdown exports.

new FroalaEditor('.selector', {

  preserveListMarkerStyle: false

});

// Whole bold item → <li><strong>Item</strong></li>

Images and Video Events

  • image.replaced now receives a third argument, old_src, so you can clean up the replaced file on your server.
  • video.replaced now receives the upload response (or undefined for URL/embed) and old_src.
  • Existing handlers keep working since the original arguments are unchanged.

Collaboration and Selection

  • Selection highlight stay visible when typing in popup fields. When focus moves into a text field inside a popup (like a link URL input), the selected text stays highlighted. This now also works in collaborative mode without adding extra steps to undo history. The AI Chat “Selected Content” highlight uses the same improvement.

Bug Fixes

  • Fixed Clear Formatting in Suggestion mode: it now creates a suggestion for accepted formatted content, removes pending Enter-key markers cleanly, and no longer creates cards for unformatted or still-pending content.
  • Fixed a browser spell-check correction needing two Undo actions to revert in Suggestion mode. One Undo now reverts it completely.
  • Fixed the “Add comment” floating button not appearing on mobile after press-and-hold text selection.
  • Fixed Page Break not working when the editor is placed inside an HTML table.

A full list of the remaining enhancements and bug fixes is on the v5.5.0 changelog page.

Upgrading to Froala v5.5.0

No breaking changes in this release.

Step 1: Update the package

npm install froala-editor@5.5.0

CDN

<link href="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/js/froala_editor.pkgd.min.js"></script>

If you use a framework wrapper (React, Angular, Vue, Svelte), update it to the version that supports Froala 5.5.0.

Step 2: Clear caches

Clear your build and browser caches so the new files load.

Step 3: Test key workflows

Check your main editing flows, especially collaboration, AI features, and any custom plugins.

Upgrading from an older major version? Follow the Froala 3 to Froala 5 migration guide.

What’s Coming Next

With this release’s AI work delivered, our focus now widens to architecture and accessibility. Here is what the team is working on:

  • Headless Mode: Full Coverage (In progress). We’re extending headless mode beyond the initial scope in v5.5.0, so every part of the editor can be driven from your own design system without falling back to Froala’s built-in UI.
  • Accessibility Checker Plugin (Planned). A new plugin that scans content against WCAG levels A, AA, and AAA plus Section 508, with one-click fixes. We plan to include it in every license rather than as a paid add-on.

Have a use case for any of these? We’d love to hear from you.

Plans may change as development progresses.

Try Froala v5.5.0 Today

Froala v5.5.0 gives you more freedom to build the editing experience your users need, from AI-generated visuals and a fully custom UI to complete review workflows for media and links.

We would love to hear what you build with it. If something does not behave the way you expect, open an issue and tell us. Share your feedback with our team and help shape the next release.

 

Collaborative Review Workflows in Froala Without a WebSocket Server

Froala Async collaboration feature

You don’t need a WebSocket server to get practical collaboration in Froala. If your app runs on serverless platforms, shared hosting, or any infrastructure that kills idle sockets, Froala’s asynchronous mode gives you comments, track changes and version history without the pain of running a WebSocket relay.

It isn’t real-time typing: collaborators work in turns, and changes appear after a save and refresh. For most editorial workflows, where one person edits and others review, that trade-off is a net win: reviewers work at their own pace, and nothing depends on everyone being online at once.

Here’s exactly what you get:

Feature Async mode
Comments and replies ✅ Fully supported
Suggestions (track changes) ✅ Fully supported
Version history ✅ Fully supported
Live cursors ❌ Not available
Instant sync while typing ❌ Not available

For review workflows, which is what most teams actually want, this trade is often a better fit. Reviewers work at their own pace, and nobody needs to be online at the same time.

Key Takeaways

  • No WebSocket needed. Leaving out realTime.syncUrl switches Froala into async mode, and three REST modules from the Node SDK handle everything else.
  • Review features work fully. Comments, replies, suggestions and version history all persist to a single SQLite file.
  • Collaborators take turns. There are no live cursors, and updates appear after the other person saves and you refresh.
  • The document follows last save wins. Comments and suggestions never conflict, so the simplest pattern is one editor and any number of suggesters.
  • You can upgrade later. Adding real-time sync reuses the same persistence routes, so nothing you build here is thrown away.

What We’ll Build

We’ll build a small page where one person writes a draft and another person reviews it with comments and suggestions. Everything is saved, so each person sees the other’s work after refreshing. The backend is three REST modules and about 20 lines of code.

Prerequisites

  • Node.js 20 or newer (22 is recommended). Check by running node -v in your terminal.
  • A code editor such as VS Code.
  • A terminal, like the one built into VS Code (View → Terminal).

That’s it. No database to install; SQLite is just a file on disk.

Step 1: Create the Project

Open your terminal and run these commands one at a time:

mkdir froala-async-review 
cd froala-async-review 
npm init -y 
npm install express wysiwyg-editor-node-sdk

The first two commands create a project folder and move into it. npm init -y creates a package.json file, which is your project’s ID card. The last line installs Express, which is our web server, and Froala’s Node SDK, which includes the collaboration modules. SQLite support (better-sqlite3) comes with the SDK automatically.

Step 2: Create the Server

Create a file called server.js in your project folder. We’ll build it in three small pieces.

Piece 1: Load the tools. 

const path = require('path');       // Builds file paths that work on every OS
const fs = require('fs');           // Lets us create folders
const express = require('express'); // Our web server framework

// Grab the three collaboration modules we need from Froala's SDK
const { CollabPersistence, VersionControl, AsyncSave } = require('wysiwyg-editor-node-sdk');

The code loads the libraries we installed. Notice that Collaborative, the WebSocket module, is missing from the list. We don’t need it.

Piece 2: Set up Express.

const app = express();

// Understand JSON requests (used for comments, suggestions and versions)
app.use(express.json({ limit: '5mb' }));

// Understand form-encoded requests (used by Froala's save plugin)
app.use(express.urlencoded({ extended: true, limit: '5mb' }));

// Serve our HTML page from the "public" folder
app.use(express.static(path.join(__dirname, 'public')));

This is where many developers get stuck, so here’s why both parsers matter. Comments and suggestions arrive as JSON, but Froala’s save plugin sends the document as a form. Without express.urlencoded(), every save fails with a quiet 400 error and your document never persists.

Piece 3: Attach the collaboration routes and start the server.

// SQLite needs its folder to exist before it can create the database file
const dataDir = path.join(__dirname, 'data');
fs.mkdirSync(dataDir, { recursive: true });
const dbPath = path.join(dataDir, 'collab.db');

// All three modules share one database file
CollabPersistence.attachRoutes(app, { dbPath }); // Comments and suggestions
VersionControl.attachRoutes(app, { dbPath });    // Version history
AsyncSave.attachRoutes(app, { dbPath });         // The document content itself

app.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

Each attachRoutes call adds a set of REST endpoints to your server. Together they create:

  • /collab/:docId/comments for comment cards and replies
  • /collab/:docId/suggestions for track-change suggestions
  • /collab/:docId/versions for saved snapshots
  • /collab/:docId/content for the document’s HTML

The :docId part is the room number where collaborators meet. Everyone using the same docId works on the same document.

Step 3: Create the Page

Create a folder called public, and inside it, a file called index.html.

Piece 1: The HTML shell.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Froala Async Review</title>
  <!-- Froala's packaged CSS, which includes the collaboration plugin styles -->
  <link href="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/css/froala_editor.pkgd.min.css" rel="stylesheet">
</head>
<body>
  <!-- Buttons for saving the document and creating a version -->
  <button id="save">Save document</button>
  <button id="version">Save a version</button>
  <span id="status"></span>

  <!-- The editor will appear inside this div -->
  <div id="editor"></div>

  <!-- Froala's packaged JS, which includes the collaboration plugin -->
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/js/froala_editor.pkgd.min.js"></script>
  <script>
    // Our code goes here (next steps)
  </script>
</body>
</html>

The packaged build includes every plugin, collaboration included, so two files are all you need.

Piece 2: Decide who’s using the page. Paste this inside the empty <script> tag:

// Read the user's name and role from the URL, for example ?name=Sam&role=suggester
const params = new URLSearchParams(location.search);
const userName = params.get('name') || 'Guest';
const userRole = params.get('role') || 'editor';        // editor, suggester or viewer
const userId = userName.toLowerCase().replace(/\s+/g, '-');

const docId = 'review-doc';                 // The room number
const base = '/collab/' + docId;            // Shared start of every URL

// Small helper that shows messages next to the buttons
function setStatus(text) {
  document.getElementById('status').textContent = text;
}

Reading identity from the URL lets you test two users in two tabs without building a login system. This is for learning only; a real app gets identity from its login session.

Piece 3: Load the saved document, then start the editor.async function start() {

  // Ask the server for the last saved version of the document
  const res = await fetch(base + '/content');
  if (res.ok) {
    const saved = await res.json();
    // Put the saved HTML into the div BEFORE the editor starts
    document.getElementById('editor').innerHTML = saved.content;
  }
  // A 404 is fine: it just means nobody has saved this document yet

  const editor = new FroalaEditor('#editor', {
    // Where the save plugin sends the document
    saveURL: base + '/content',
    // Extra fields sent with every save, so we know who saved
    saveParams: { authorId: userId, authorName: userName },
    // Autosave 5 seconds after changes (you'll also have a manual button)
    saveInterval: 5000,

    collabConfig: {
      docId: docId,
      user: { id: userId, name: userName, role: userRole },
      commentsUrl: base + '/comments',
      suggestionsUrl: base + '/suggestions',
      versionControl: { url: base + '/versions' }
      // No realTime.syncUrl here. Leaving it out is what turns on async mode.
    },

    events: {
      // Runs after a successful save
      'save.after': function () {
        setStatus('Saved at ' + new Date().toLocaleTimeString());
      },
      // Runs if the save fails
      'save.error': function () {
        setStatus('Save failed. Is the server running?');
      }
    }
  });

  // Manual save button: sends the document right now
  document.getElementById('save').addEventListener('click', () => editor.save.save());

  // Version button: stores a named snapshot in version history
  document.getElementById('version').addEventListener('click', () => {
    editor.collaborative.saveVersion('Snapshot by ' + userName, null)
      .then(() => setStatus('Version saved'));
  });
}

start();

Two important ideas here.

First, the missing syncUrl is the whole switch. Without a WebSocket address, Froala stores comments, suggestions and versions through your REST URLs instead of broadcasting them live.

Second, the order matters. Comment and suggestion cards come from the comments and suggestions tables, but the highlights in the text live inside the document HTML. That’s why we load the saved content before starting the editor: the highlights and the cards find each other on startup. It’s also why you must save the document after adding a comment, or the card will exist with no highlighted text to point to.

We wrap everything in an async function because await at the top level of a regular script is a syntax error in the browser.

Step 4: Run It

Back in your terminal, from the project folder:

node server.js

You should see Server running at http://localhost:3000. Leave this terminal open. Closing it stops the server.

⚠️ Open the page through the server, not by double-clicking the HTML file. A file:// page can’t reach your /collab routes.

Step 5: Test a Review Workflow

Open two browser tabs:

  • Tab A (the writer): http://localhost:3000/?name=Alex&role=editor
  • Tab B (the reviewer): http://localhost:3000/?name=Sam&role=suggester

Now walk through a real review:

  1. In Tab A, type a paragraph and click Save document.
  2. Refresh Tab B. Sam sees Alex’s paragraph. 🎉
  3. In Tab B, Sam starts in Suggesting mode because of the suggester role. Change a word, then select a sentence and add a comment. Click Save document.
  4. Refresh Tab A. Alex sees Sam’s comment and suggestion in the side panel.
  5. Alex accepts the suggestion, replies to the comment, and clicks Save document.
  6. Click Save a version to capture the reviewed draft.

Congratulations! You just built a complete review workflow with a server that has no WebSocket at all. Stop the server, start it again, and refresh: everything is still there, because it lives in data/collab.db.

The One Rule of Async Mode

Since there’s no live sync, the document itself follows last save wins. If Alex and Sam both edit the same paragraph at the same time, the later save replaces the earlier one.

Comments and suggestions are safe, because each one is its own database row. The document text is the only thing to coordinate. The easiest pattern: one person owns the text as the editor, and everyone else reviews as a suggester. That’s how most editorial teams already work.

Troubleshooting

Saves fail with a 400 error. Check that express.urlencoded() is in server.js. The save plugin sends form data, not JSON.

The page loads but nothing saves. Make sure you opened http://localhost:3000, not the file directly, and that the terminal running node server.js is still open.

Comments show in the panel but the text isn’t highlighted. The comment was stored, but the document wasn’t saved afterward. Click Save document after reviewing.

EADDRINUSE when starting the server. Something else is using port 3000, often an old copy of your server. Close the other terminal or change 3000 to 3001 in server.js.

npm install fails while building better-sqlite3. Your Node version is probably too old. Upgrade to Node 22 and reinstall.

I don’t see Sam’s changes. In async mode, updates appear when you refresh, after the other person has saved.

Before Going to Production

This demo is intentionally simple. Before real users touch it:

  • Add authentication. Put your own login check in front of the routes with app.use('/collab', requireLogin) before the attachRoutes calls, and check that each user may access that docId.
  • Replace URL identity. Take name and role from the user’s session, never from the query string.
  • Sanitize HTML on the server. The content endpoint stores HTML exactly as received, and our page inserts it with innerHTML. Clean it with a library such as sanitize-html before saving.

Frequently Asked Questions

Does my backend have to be Node.js?
No. The editor only talks to plain REST endpoints, so any language can serve them. The Node SDK is the quickest route, but a PHP, Rails or Django app can implement the same routes by following the request and response shapes in the backend API reference.

Will this run on a serverless platform as written?
The routes will, but the SQLite file won’t survive. Serverless functions usually get a temporary disk that’s wiped between runs, so your data would disappear. On serverless, keep the same endpoints and store the data in a hosted database instead.

Can I see other people’s changes without refreshing the page?
Partly. Calling editor.collaborative.syncCollabData() re-fetches comments and suggestions, and editor.collaborative.checkForUpdates() refreshes the version list. You could run these on a button or a timer. The highlighted text lives in the document itself, though, so a refresh is still the most reliable way to see everything a reviewer did.

Do I have to click Save every time?
Not usually. The save plugin autosaves a few seconds after you stop typing, based on saveInterval. The manual button is a safety net, and it’s handy right after accepting suggestions or adding comments.

Can versions be created automatically?
Yes. Add autoSaveEnabled: true inside versionControl, and Froala saves a snapshot every 60 seconds by default. Use autoSaveInterval to change the timing, in milliseconds.

What happens if two people edit the text at the same time?
The later save replaces the earlier one. Comments and suggestions are stored separately and are never overwritten this way. If simultaneous editing is common for your team, that’s the signal to add the real-time relay.

Where to Go Next

When you’re ready for live cursors, the collaboration backend tutorial adds the WebSocket relay on top of the same persistence routes you built today. Your comments, suggestions and versions carry over unchanged. For every option and endpoint, see the collaboration plugin docs and the backend API reference, or try it live in the collaborative editing demo.

graphical user interface, text

How to Integrate a Rich Text Editor in a React TypeScript Project

React-TypeScript Rich Text Editor Tutorial

Quick answer: Install froala-editor and react-froala-wysiwyg, and import the stylesheets and plugins once.  Then render the editor as a controlled component with model and onModelChange, and type your config with Partial<FroalaOptions> so events and options are checked by the compiler.

One of the easiest parts of adding a React TypeScript rich text editor within your application is rendering an editable area. The most challenging parts are content, state, configuration, plugins, events, and the editor’s overall communication with your app. On top of that, TypeScript adds one essential requirement. The integration must smoothly fit into a typed codebase.

This tutorial walks through that integration with Froala. Every snippet below was compiled with strict: true and built with Vite, using React 19.3, TypeScript 5.9, Vite 7.3, froala-editor 5.4.0, and react-froala-wysiwyg 5.4.0. 

What Do You Need to Build a React TypeScript Rich Text Editor?

You need Node.js, npm, and either an existing React TypeScript app or a new one. For a new project, scaffold with Vite. React’s own documentation no longer recommends Create React App, so this tutorial skips it.

npm create vite@latest my-editor-app -- --template react-ts
cd my-editor-app
npm install

The Vite template ships with strict, verbatimModuleSyntax and noUnusedLocals enabled. All code below passes under those settings.

How to Install Froala in a React TypeScript Project

This tutorial does not require a Froala activation key to run the Froala editor. We are using the free and unlicensed version of Froala. When you are ready for production, you can always add the key setup (see the production checklist near the end).

npm install froala-editor react-froala-wysiwyg

Centralize the stylesheets and plugin bundle in one file, src/froala-setup.ts, and import it once from your entry point. This keeps every component free of repeated side-effect imports.

// src/froala-setup.ts
import 'froala-editor/css/froala_editor.pkgd.min.css';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/js/plugins.pkgd.min.js';

 

// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './froala-setup';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

The two stylesheets do different jobs. froala_editor.pkgd.min.css styles the editor UI, including the toolbar. froala_style.min.css styles the content itself (the .fr-view class), so you also want it wherever you render saved HTML outside the editor.

plugins.pkgd.min.js loads every plugin, which is convenient in development. For production, import only the plugins you enable, for example froala-editor/js/plugins/link.min.js, to keep the bundle smaller.

How Do You Add the Rich Text Editor Component?

Here is BasicEditor.tsx, the first working example. In practice, the wrapper’s default export doesn’t always resolve to the component itself under Vite. Depending on the exact Vite/react-froala-wysiwyg version pairing, the import can resolve to the module’s raw exports object instead, which throws Element type is invalid... but got: object at render time. Rather than debug that per project, unwrap it once in a typed helper and import the component from there everywhere else:

// src/FroalaEditor.ts
import * as FroalaModule from 'react-froala-wysiwyg';
import type FroalaEditorComponent from 'react-froala-wysiwyg';
type ModuleShape = { default?: { default?: unknown } };
const mod = FroalaModule as ModuleShape;
const resolved = (mod.default?.default ?? mod.default ?? FroalaModule) as typeof FroalaEditorComponent;
export default resolved;

 

// src/BasicEditor.tsx
import { useState } from 'react';
import FroalaEditorComponent from './FroalaEditor';

export default function BasicEditor() {
  const [content, setContent] = useState<string>('<p>Hello from React!</p>');

  return (
    <>
      <FroalaEditorComponent
        tag="textarea"
        model={content}
        onModelChange={setContent}
      />
      <h3>Live React state</h3>
      <pre>{content}</pre>
    </>
  );
}

tag tells the wrapper which element to initialize the editor on; textarea is the standard choice for a full editing surface. model and onModelChange connect the editor to React state, and the <pre> underneath renders that state live so you can watch the HTML string change as you type.

Drop this component into a page, and you have a working, styled editor. Everything else in the tutorial builds on it.

How Do You Manage Editor Content in React?

model and onModelChange make this a standard controlled component. model is the HTML the editor should show; onModelChange fires with the current HTML string whenever the editor records a content change. setContent from useState already matches the expected signature, so no wrapper function is needed.

Two details about update timing are worth knowing. The wrapper drives onModelChange from Froala’s contentChanged event, which the editor batches during typing using its typing timer rather than firing on every keystroke. If you need updates on each key release, set immediateReactModelUpdate: true in the config; the wrapper then also listens to keyup. That costs performance on large documents, so leave it off unless you need it.

Keep content in local state for a single self-contained editor. Once it needs to reach a form, an API call, or a sibling component, lift it to a parent or a shared state layer, as PostForm does later in this tutorial.

How Do You Configure a React Rich Text Editor with TypeScript?

Inline JSX options get unwieldy past two or three keys. Pull them into a config object, and type it with Partial<FroalaOptions> from froala-editor. That single annotation is what gives you autocomplete on option names and type errors on wrong values. It also types the this context inside event callbacks, which matters in the next section.

// src/ConfiguredEditor.tsx
import { useState } from 'react';
import type { FroalaOptions } from 'froala-editor';
import FroalaEditorComponent from './FroalaEditor';

const config: Partial<FroalaOptions> = {
  placeholderText: 'Start writing...',
  charCounterCount: true,
  heightMin: 300,
  toolbarButtons: ['bold', 'italic', 'underline', 'insertLink'],
};

export default function ConfiguredEditor() {
  const [content, setContent] = useState<string>('');

  return (
    <FroalaEditorComponent
      tag="textarea"
      config={config}
      model={content}
      onModelChange={setContent}
    />
  );
}

 A shared config object reads better than a JSX prop with a dozen inline keys, and the same object can back every editor instance in the app instead of retyping toolbar buttons per screen.

One note on where the strictness lives. The wrapper’s own prop types are loose: config, onModelChange and onManualControllerReady are declared as object. TypeScript will not catch a wrongly shaped config at the JSX boundary. It catches it where you declare the config as Partial<FroalaOptions>, and where you type your own component’s props (see the reusable component section). Put the types there.

How Do You Handle Froala Editor Events in TypeScript?

Events go in the events key of the config. Because the config is typed, each callback’s this is typed as FroalaEditor, so this.html.get() resolves to a real method with a string return type. This is the only reason the example compiles under strict: with an untyped object literal, TypeScript would infer this as the events object and reject this.html.

// src/EventsEditor.tsx
import { useState } from 'react';
import type { FroalaOptions } from 'froala-editor';
import FroalaEditorComponent from './FroalaEditor';

const config: Partial<FroalaOptions> = {
  placeholderText: 'Start writing...',
  events: {
    initialized: function () {
      console.log('Editor ready');
    },
    contentChanged: function () {
      // 'this' is the editor instance, typed as FroalaEditor
      console.log(this.html.get());
    },
    focus: function () {
      console.log('Editor focused');
    },
    blur: function () {
      console.log('Editor lost focus');
    },
  },
};

export default function EventsEditor() {
  const [content, setContent] = useState<string>('');

  return (
    <FroalaEditorComponent
      tag="textarea"
      config={config}
      model={content}
      onModelChange={setContent}
    />
  );
}

Any callback that reads this must be a regular function. Arrow functions capture the surrounding this and cannot be bound to the editor; a callback that ignores this can be either.

contentChanged is the event behind onModelChange, so for React state you already have it. Use it directly when you need something that is not state, such as an autosave timer or validation that should run without a re-render. Froala exposes many more events, including paste and image upload hooks; the events documentation has the full list.

How Do You Access the Editor Instance?

Props and config cover most day-to-day needs. Occasionally you need the instance itself, for example, to run a command from a button outside the editor or read its HTML on demand. The wrapper’s onManualControllerReady prop hands you an object with initialize, destroy, and getEditor, and defers initialization until you call initialize() yourself.

// src/ManualControlEditor.tsx
import { useRef, useState } from 'react';
import type FroalaEditor from 'froala-editor';
import FroalaEditorComponent from './FroalaEditor';

interface ManualControls {
  initialize: () => void;
  destroy: () => void;
  getEditor: () => FroalaEditor | null;
}

export default function ManualControlEditor() {
  const controlsRef = useRef<ManualControls | null>(null);
  const [snapshot, setSnapshot] = useState<string>('');

  const handleControllerReady = (controls: ManualControls) => {
    controlsRef.current = controls;
    controls.initialize();
  };

  const handleGetHtml = () => {
    const editor = controlsRef.current?.getEditor();
    if (!editor) {
      setSnapshot('(editor not mounted)');
      return;
    }
    setSnapshot(editor.html.get());
  };

  return (
    <>
      <div className="button-row">
        <button onClick={handleGetHtml}>Read current HTML</button>
        <button onClick={() => controlsRef.current?.destroy()}>Destroy</button>
        <button onClick={() => controlsRef.current?.initialize()}>Re-initialize</button>
      </div>
      <FroalaEditorComponent
        tag="textarea"
        onManualControllerReady={handleControllerReady}
      />
      <pre>{snapshot || '(no snapshot yet)'}</pre>
    </>
  );
}

import type FroalaEditor from 'froala-editor' is a type-only import. It is erased at build time and gives getEditor a real return type instead of an implicit any. getEditor returns null until the editor has mounted, so guard before calling anything on it. The wrapper does not export a type for the controls object, which is why ManualControls is declared locally.

How Do You Create a Reusable React Editor Component?

Once the pieces work in isolation, wrap them in one typed component the rest of the app can drop in anywhere. Keep the shared config in its own module so the form example and any future editor use the same toolbar.

// src/froala-config.ts
import type { FroalaOptions } from 'froala-editor';

export const baseConfig: Partial<FroalaOptions> = {
  placeholderText: 'Start writing...',
  charCounterCount: true,
  heightMin: 200,
  toolbarButtons: ['bold', 'italic', 'underline', 'insertLink'],
};

 

// src/RichTextEditor.tsx
import FroalaEditorComponent from './FroalaEditor';
import { baseConfig } from './froala-config';

export interface RichTextEditorProps {
  value: string;
  onChange: (value: string) => void;
}

export default function RichTextEditor({ value, onChange }: RichTextEditorProps) {
  return (
    <FroalaEditorComponent
      tag="textarea"
      config={baseConfig}
      model={value}
      onModelChange={onChange}
    />
  );
}

This is where the TypeScript contract becomes real. Any component rendering RichTextEditor must supply a string value and a function that accepts one. A toolbar change or a design update now happens in one file instead of once per screen.

How Do You Use the Editor Inside a React Form?

The reusable component slots into a form like any controlled input. This example checks for empty content before submitting, disables the button while the request is in flight, and surfaces success and failure states.

// src/PostForm.tsx
import { useState, type FormEvent } from 'react';
import RichTextEditor from './RichTextEditor';

// Empty means no text and no embedded media. Parsing with DOMParser decodes
// every entity form (&nbsp;, &#160;, &#xA0;), which a regex on the raw string
// does not.
const isBlank = (html: string) => {
  const body = new DOMParser().parseFromString(html, 'text/html').body;
  const hasMedia = body.querySelector('img, video, iframe, table') !== null;
  return !hasMedia && (body.textContent ?? '').trim().length === 0;
};

export default function PostForm() {
  const [body, setBody] = useState<string>('');
  const [error, setError] = useState<string | null>(null);
  const [status, setStatus] = useState<string | null>(null);
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async (event: FormEvent) => {
    event.preventDefault();
    setError(null);
    setStatus(null);

    if (isBlank(body)) {
      setError('Post content cannot be empty.');
      return;
    }

    setSubmitting(true);
    try {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: body }),
      });
      if (!res.ok) throw new Error(`Request failed: ${res.status}`);
      setStatus('Published successfully.');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <RichTextEditor value={body} onChange={setBody} />
      {error && <p role="alert">{error}</p>}
      {status && <p role="status">{status}</p>}
      <button type="submit" disabled={submitting}>
        {submitting ? 'Publishing...' : 'Publish'}
      </button>
    </form>
  );
}

The emptiness check parses the HTML instead of running a regex over it. An editor can hold <p>&nbsp;</p> or <p>&#160;</p>, both non-empty as raw strings and both empty as content; textContent decodes every entity form and trim() removes the resulting non-breaking spaces. The media query keeps an image-only post from being rejected as blank. Adjust the selector to whatever your app counts as content. Wrapping fetch in try/catch means a failed request becomes a visible error rather than a silent no-op.

How to Integrate a Rich Text Editor in a React TypeScript Project

 

Common React and TypeScript Integration Issues

The toolbar is missing or unstyled

If the toolbar renders as unstyled buttons, froala_editor.pkgd.min.css did not load. Check that froala-setup.ts is imported from your entry point, not just from one component. If there is no toolbar at all and the CSS is loading, the editor did not initialize: the usual cause is passing onManualControllerReady and never calling initialize(). If the toolbar looks fine but saved content renders without styling elsewhere, the missing file is froala_style.min.css.

A toolbar button appears but does nothing, or does not appear at all

The plugin behind the button is not loaded. insertLink needs the link plugin, charCounterCount needs the char counter plugin, and so on. Either import plugins.pkgd.min.js or the individual plugin file. Listing a button in toolbarButtons does not load its plugin.

TypeScript cannot resolve react-froala-wysiwyg/FroalaEditorView

The wrapper ships its types in lib/index.d.ts, and that file also declares the subpath modules such as react-froala-wysiwyg/FroalaEditorView. TypeScript only loads it when the root package is referenced somewhere in the program. If a file imports only a subpath, add a reference directive at the top of that file:

/// <reference types="react-froala-wysiwyg" />
import FroalaEditorView from 'react-froala-wysiwyg/FroalaEditorView';

Do not fix this with a bare declare module 'react-froala-wysiwyg/FroalaEditorView';. That is a shorthand ambient declaration and makes the import any.

this.html is an error inside an event callback

The config object is untyped. Declare it as Partial<FroalaOptions> and the events map picks up this: FroalaEditor from Froala’s FroalaEvents interface. See the events section above.

Editor content and React state disagree

Content is being set from more than one place, typically model plus a direct call to editor.html.set(). Keep a single flow: the editor updates state through onModelChange, and state flows back through model. If you must set HTML imperatively, do it through state.

The editor initializes twice in development

Older versions of the wrapper rendered two editors under React 18’s StrictMode because StrictMode runs each component’s mount cycle twice in development. Wrapper 5.4 destroys and recreates the editor cleanly across that cycle; we verified a single toolbar and a single .fr-box under StrictMode with React 19. If you see a doubled editor, upgrade react-froala-wysiwyg rather than removing StrictMode, which would hide other lifecycle bugs in your own code.

What Should You Consider Before Using a React Rich Text Editor in Production?

A working demo and a production-ready editor are not the same thing. Before you ship:

  • Sanitize HTML server-side. Froala cleans HTML on the client through htmlAllowedTags, htmlAllowedAttrs, and htmlRemoveTags, and will use DOMPurify if it is present on the page, but client-side cleanup is never a substitute for server-side sanitization of user-submitted content.
  • Set the license key from an environment variable and confirm the editor no longer shows the unlicensed notice on your production domain.
  • Plan the file and image upload workflow: where files land and how they are served back. Froala’s image upload documentation covers the server-side pieces.
  • Import only the plugins you enable, and confirm they are in the production bundle.
  • Lock down toolbarButtons in a shared config so every instance looks and behaves the same.
  • Test keyboard navigation and screen readers against your accessibility requirements.
  • Test mobile and touch behavior, not only desktop layouts.
  • Measure performance with realistic document sizes, not a two-paragraph test.
  • Decide where content persists and what the user sees when a save fails.
  • Keep React, TypeScript, and both Froala packages on current supported versions, and pin them.

Build Your React TypeScript Editor with Froala

Across this tutorial, you installed the editor and its React wrapper and rendered a working editor. Then you connected it to typed React state, added a typed config with events, reached the editor instance when props were not enough, and packaged the result into a reusable component wired into a real form.

The React integration documentation covers the remaining configuration options, events, plugins, and API methods. From there, start a Froala trial and drop the same configuration into your own project.

FAQ

Can I use a rich text editor with React and TypeScript?

Yes. With Froala’s React wrapper, the editor is a controlled component, the config is typed through Partial<FroalaOptions>, event callbacks get a typed this, and the underlying instance is reachable through onManualControllerReady. All of that is shown above.

How do I get HTML from a React rich text editor?

Read the state value that onModelChange keeps up to date, or call this.html.get() inside an event callback, or call editor.html.get() on the instance from getEditor(). Each returns a plain HTML string you can store or send to an API.

Should a React rich text editor be a controlled component?

For reusable components and forms, yes. One flow (onModelChange into state, model back into the editor) is easier to reason about than mixing controlled and uncontrolled patterns on the same editor.

What should I look for in a rich text editor for a React TypeScript project?

An official React wrapper, shipped TypeScript definitions for the editor’s options and events, a documented event system, and a way to reach the underlying instance when props are not enough. An editor that covers those four points will fit a typed codebase without extra glue.

What matters most in a rich text editor for a complex React app?

Plugin architecture and typed configuration matter more than toolbar button count. An editor that exposes custom commands and the underlying instance, with a full event lifecycle, scales better in a large application than one that only covers basic formatting.

What licensing models exist for React rich text editors?

Open-source editors ship under permissive or copyleft licenses and leave maintenance to you. Commercial editors sell licenses on different bases: some per developer seat and some per editor load. Froala is licensed per product and per domain with unlimited users (Professional covers one product on three domains; Enterprise covers unlimited products and domains, including SaaS and OEM use). Check the current terms on the pricing page before committing a production app to any editor.

Are there commercial rich text editors for React that support TypeScript?

Yes. Froala ships type definitions with both the editor and the React wrapper, and several other commercial editors do the same. The trade is a license fee for maintained types and a documented upgrade path, which matters most when the editor is load-bearing in a production system.

Where can I find a customizable React TypeScript rich text editor package?

Start with the vendor’s GitHub repository and the npm package page. The plugin system and the TypeScript types should be documented in one place, which is what this tutorial walked through for Froala.

What are typical pricing plans for React rich text editors?

Open-source editors are free to use and yours to maintain, and some vendors charge per editor load on top of a free tier. Froala is a flat annual or perpetual license per product and domain allowance, with no per-user or per-load charge, and an Enterprise tier for SaaS and OEM products. Confirm current pricing on the vendor’s page rather than relying on older guides.

 

graphical user interface, text

Building a Production-Ready Collaboration Backend for Froala

collaboration backend for Froala

Two people or more, one document, no lost edits. Froala 5.3 solves the hardest part of real‑time collaboration. It ships Yjs in the browser to merge concurrent keystrokes into a single coherent document, so you don’t need to wrestle with CRDTs or conflict math.

What is left for you is a small, reliable server: a WebSocket relay to pass updates between peers and a persistence layer to keep comments, versions, and the canonical copy. Neither one is exotic. This guide shows how to build those two ordinary pieces with Froala’s Node.js SDK and how to avoid the common pitfalls that turn a demo into a maintenance problem.

The Froala Collaboration Backend Server’s Function

The collaboration backend server has two responsibilities:

Passing messages between browsers

A WebSocket is a connection that stays open in both directions. Froala’s relay groups those connections by docId, which is simply the room number where collaborators meet. When one browser sends an update, the relay rebroadcasts it verbatim to every other browser in the same room. It never parses the message, never validates it, never stores it. All the intelligence is in the clients.

Persistence

The relay forgets everything the moment the last person closes their tab. So comments, track-change suggestions, and version snapshots travel over a second path: plain REST endpoints backed by SQLite. That’s what survives an empty room, a browser refresh, and a server restart.

The Froala Node.js SDK ships these as four modules you compose as needed.

Module What it handles Transport
Collaborative Live peer sync, presence, cursors WebSocket
CollabPersistence Suggestions and comments REST + SQLite
VersionControl Named and automatic snapshots REST + SQLite
AsyncSave Latest document content in offline mode REST + SQLite

collaboration server components

Why this matter? If you mount only the relay, everything works beautifully until the last person closes the tab, and then it is gone. Real-time sync and persistence are two separate responsibilities, and you need both.

Real-time or asynchronous: pick one first

The Froala Collaborative plugin runs in two modes, and which one you are in is decided by a single value. Setting collabConfig.realTime.syncUrl turns on live sync across every collaborative feature. Leaving it out drops the plugin into asynchronous, offline-first mode where the REST endpoints are the only source of truth. Decide this before you write the server, because it changes what you have to mount.

Real-time Asynchronous
Set realTime.syncUrl plus the REST URLs REST URLs only
Needs All four modules The three REST modules
Feels like Cursors and edits appear instantly Changes appear on load or on refresh
Suits Live co-editing sessions Review cycles, flaky networks, simpler infrastructure

Prerequisites for the Froala Collaboration Backend

You need:

  • Node.js 22 or newer
  • npm

1. Install the SDK

mkdir froala-collab && cd froala-collab
npm init -y
npm install express wysiwyg-editor-node-sdk

That is everything the backend needs to run. The SQLite driver, better-sqlite3, is a regular dependency of the SDK and installed automatically. It is a native module, so on a bare Linux container without a prebuilt binary available you may also need build tools (python3, make, g++) present during install. Two small packages for CORS and rate limiting appear later, once we get to hardening.

2. Create the folder your database lives in

SQLite will create the database file for you. It will not create the directory that file goes in.

Common first error. Pointing dbPath at data/collab.db when data/ does not exist throws SQLITE_CANTOPEN on startup. The fix is one line in the server, shown next, so the folder is always there before any module opens the file.

3. Write the server

Create server.js and build it up in pieces.

const http    = require('http');
const path    = require('path');
const fs      = require('fs');
const express = require('express');

// Pull in only the collaboration modules you plan to mount.
const {
  Collaborative,      // WebSocket relay
  CollabPersistence,  // suggestions + comments
  VersionControl,     // version snapshots
  AsyncSave,          // offline content save/load
} = require('wysiwyg-editor-node-sdk');

Next, guarantee the database folder exists, then point every module at one shared file.

const dataDir = path.join(__dirname, 'data');
// recursive:true creates the folder if missing and stays quiet if it exists.
fs.mkdirSync(dataDir, { recursive: true });

const dbPath = path.join(dataDir, 'collab.db');

Now the Express app. The body limit deserves a moment of attention.

const app = express();

// Version snapshots are entire HTML documents, not small form posts.
// Express defaults to 100kb and answers 413 above that, which shows up
// in the browser as "saving a version silently does nothing".
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ extended: true, limit: '5mb' }));

Now the piece that catches nearly everyone on the first run. Your editor page is almost certainly served from a different origin than port 3000, and a browser will block every REST call until you say otherwise.

// npm install cors
const cors = require('cors');

// Name the exact origin your page is served from. Not '*', because
// credentials:true and a wildcard origin are mutually exclusive.
app.use(cors({
  origin: process.env.APP_ORIGIN,   // e.g. http://localhost:5173
  credentials: true,                // lets session cookies ride along
}));

What this looks like when it is missing. The editor loads, cursors sync fine over the WebSocket, and every comment request fails with a CORS message in the console. The relay is unaffected because WebSockets are not subject to the same-origin policy, which is exactly why the failure looks so selective.

// Each call registers its REST routes and creates its tables on first run.
// Sharing one dbPath keeps everything in a single file.
CollabPersistence.attachRoutes(app, { dbPath });
VersionControl.attachRoutes(app, { dbPath });
AsyncSave.attachRoutes(app, { dbPath });

// getStats() reports live rooms and connections. Useful for monitoring.
app.get('/health', (req, res) => res.json(Collaborative.getStats()));

Finally, put the relay and the REST API on the same port.

// Build the HTTP server yourself so both can share it.
const server = http.createServer(app);

// This line tells Froala to listen for WebSocket upgrades on our server.
Collaborative.attachToServer(server);

server.listen(3000, () => {
  console.log('Collaborative backend on http://localhost:3000');
});

attachToServer or createServer? Use attachToServer() when REST and the relay share a port, which is almost always. createServer() is for running the relay alone on its own port, with nothing else on it.

4. Run it and confirm it is alive

node server.js

In a second terminal:

curl http://localhost:3000/health

// {"docs":0,"clients":0}

curl http://localhost:3000/collab/my-doc/comments

// []

An empty array is a success. It means the route is mounted, the database opened, and the table exists. Notice the URL shape: every REST endpoint is namespaced under /collab/:docId/, and that docId has to match the one the WebSocket connection uses.

5. Put the editor on the page

The backend is only half the system. Load the packaged build, which ships every plugin including the collaborative one.

<link rel="stylesheet" href="/node_modules/froala-editor/css/froala_editor.pkgd.min.css">
<link rel="stylesheet" href="/node_modules/froala-editor/css/froala_style.min.css">

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

<script src="/node_modules/froala-editor/js/froala_editor.pkgd.min.js"></script>
<script>
  const editor = new FroalaEditor('#editor', {
    // Read the key from injected config. 
    key: window.APP_CONFIG.froalaKey,

    // collabConfig arrives in the next step.
  });
</script>

If you are using a bundler and a modular build instead, you have to import the collaborative plugin’s JavaScript and its stylesheet by their exact paths under node_modules/froala-editor, because the core alone does not include it.

6. Tell the editor these endpoints exist

This is the step that trips up almost everybody. Mounting routes on your server does not make the editor call them. The editor only talks to URLs you hand it in collabConfig.

new FroalaEditor('#editor', {
  key: window.APP_CONFIG.froalaKey,   

  collabConfig: {
    // The room number. Same value as the WebSocket path.
    docId: 'my-doc',

    // Display information only. The server decides who this really is.
    user: { id: 'user-42', name: 'Priya', role: 'editor' },

    // Persistence. Without these three, comments, suggestions and
    // versions live only for the duration of the session.
    commentsUrl:    'http://localhost:3000/collab/my-doc/comments',
    suggestionsUrl: 'http://localhost:3000/collab/my-doc/suggestions',
    versionControl: { url: 'http://localhost:3000/collab/my-doc/versions' },

    // The live wire. Omit this object entirely for offline-first mode.
    realTime: { syncUrl: 'ws://localhost:3000' }
  },

  // Async mode stores the document itself through AsyncSave.
  saveURL: 'http://localhost:3000/collab/my-doc/content'
});

Two things worth internalising. First, realTime.syncUrl is the master switch: set it and everything synchronises live, leave it out and the plugin falls back to asynchronous offline-first mode using the REST URLs alone. Second, in real-time mode those REST URLs are still doing work. Live sync shares data between connected peers, but it is the URLs that write it to disk and fetch it back on the next page load.

Do not hardcode the document id

The configuration above repeats my-doc in four places plus the WebSocket path. In a real application that is five chances to typo a room number, and a mismatch between the REST docId and the WebSocket docId produces a document that syncs live but persists into a different bucket. Build the whole object from one value instead.

function collabConfigFor(docId, user) {
  const origin = window.APP_CONFIG.collabOrigin;     // https://collab.example.com
  const base   = `${origin}/collab/${encodeURIComponent(docId)}`;

  return {
    docId,
    user,                                            // display information only

    commentsUrl:    `${base}/comments`,
    suggestionsUrl: `${base}/suggestions`,

    versionControl: {
      url: `${base}/versions`,
      autoSaveEnabled: true,
      // Default is 60000. Every interval writes a full copy of the
      // document, so five minutes is kinder to your disk and to the
      // retention job later in this guide.
      autoSaveInterval: 300000,
    },

    realTime: {
      // http becomes ws, https becomes wss, without a second constant.
      syncUrl: origin.replace(/^http/, 'ws'),
      reconnectDelay: 2000,
    },
  };
}
new FroalaEditor('#editor', {
  key: window.APP_CONFIG.froalaKey,
  htmlExecuteScripts: false,
  collabConfig: collabConfigFor('q3-report', currentUser),
  saveURL: `${origin}/collab/q3-report/content`,
});

7. Load the document’s starting content

Here’s the consequence of the relay being amnesiac: when the first person opens a document, there’s no peer to sync from. The editor starts empty. Nothing in the SDK fills that gap. The starting content has to come out of your storage and into the element before the editor initializes.

In asynchronous mode, and in real-time mode where you also persist through AsyncSave, that content is one request away.

async function startingHtml(docId) {
  const res = await fetch(`/collab/${encodeURIComponent(docId)}/content`, {
    credentials: 'include',
  });

  // 404 is the normal answer for a document nobody has saved yet.
  if (res.status === 404) return '';

  // Anything else that is not a success is a real problem. Fail loudly
  // rather than opening an empty editor that looks like data loss.
  if (!res.ok) {
    throw new Error(`Could not load document: ${res.status}`);
  }

  const { content } = await res.json();
  return content;
}

async function boot(docId) {
  // Seed the element first, then let Froala take over the filled element.
  document.querySelector('#editor').innerHTML = await startingHtml(docId);

  new FroalaEditor('#editor', {
    key: window.APP_CONFIG.froalaKey,
    htmlExecuteScripts: false,
    collabConfig: collabConfigFor(docId, currentUser),
    saveURL: `${window.APP_CONFIG.collabOrigin}/collab/${docId}/content`,
  });
}

boot('q3-report').catch((err) => {
  console.error(err);
  document.querySelector('#editor').textContent =
    'This document could not be loaded. Refresh to try again.';
});

Every client seeds from the same stored row, so peers start from identical state and live updates apply on top of it. Server-rendering the same HTML into the element works just as well and avoids the extra round trip. What does not work is seeding different clients with different content, because each one then contributes its own starting text to the shared document.

Keep the stored copy current

Loading is only half of it. That stored copy also has to be kept up to date, and this matters more than it first appears. Comment and suggestion records come from commentsUrl and suggestionsUrl, but the highlights that attach them to specific text live in the document content itself. A document that is never saved will reload with its comment cards intact and nothing in the text to attach them to.

saveURL: `${window.APP_CONFIG.collabOrigin}/collab/${docId}/content`,
saveParam: 'body', // the field AsyncSave expects 
saveInterval: 5000, // attempt a save 5 seconds after a change

Three details trip people up here:

  • The save plugin posts form-encoded, not JSON. Your server needs express.urlencoded() mounted alongside express.json(), or every save returns 400. Step 3 covers this.
  • saveInterval is a floor, not a cadence. The plugin waits for a content change, then waits the interval, then skips the request entirely if nothing differs from the last save. Real gaps are longer than the number you set.
  • editor.save.force() does not save. It only marks the document dirty so the next attempt is not skipped. To save immediately, call editor.save.save().

Test the stale case on purpose. Open a document, make changes, and open it again in a second browser before any save has happened. That is the scenario where the stored row and the live document disagree, and it is worth knowing how your application behaves there before a user finds it.

8. Show people the connection state

A WebSocket that drops does so silently. The editor keeps accepting keystrokes, the cursor keeps blinking, and nothing suggests that the last four paragraphs are going nowhere. Three events exist precisely so that you do not ship that experience.

new FroalaEditor('#editor', {
  // ...
  events: {
    // Fires on every transition: connecting, connected, disconnected.
    'collab.connectionStatus': function (status) {
      const banner = document.querySelector('#collab-banner');
      banner.hidden = status === 'connected';
      banner.textContent = status === 'connecting'
        ? 'Reconnecting to the collaboration server'
        : 'Offline. Your changes are not being shared right now.';
    },

    // The plugin has finished initialising. Safe to edit or to run
    // data operations from here on.
    'collab.synced': function () {
      document.querySelector('#collab-banner').hidden = true;
    },

    // Someone else's change just landed in your content.
    'collab.remoteContentChanged': function () {
      markUnsaved();
    },
  },
});

The editor’s own toast notifications are a lighter-touch alternative to a banner, and a persistent one is well suited to a state that resolves on its own.

const connecting = editor.toast.show({
  title: 'Reconnecting',
  description: 'Waiting for the collaboration server.',
  interval: false,        // stays until you dismiss it
});

// Once 'collab.connectionStatus' reports connected:
connecting.hide();

Retry timing is yours to set. realTime.reconnectDelay is the wait in milliseconds before the client tries again after a drop, defaulting to two seconds. Raising it eases load on a server that is struggling; lowering it makes brief network blips less visible.

9. Test it properly

Start the backend first, then open your page in two windows: one normal, one private or a different browser. Two tabs in the same window share a session, so you will not see two distinct users. Type in one window and watch the other. A caret with a name label should appear within a second.

Then do the harder test: close both windows, reopen one, and confirm your comments and versions came back. That is the test that proves the database half is wired up.

Make Your Froala Collaboration Backend Production Ready

Everything above runs happily on localhost. Here is what has to change before it faces the internet.

production collaboration server

Identity comes from your session, not from the client

The user object in collabConfig is display information that any visitor can edit in devtools. Treat id, name, and especially role as hints for rendering cursors and nothing more. Authenticate the WebSocket upgrade request and every REST call, then derive the real user and the real role from your session on the server. Authorization is per document: whether this person may join this docId is a server decision, never a client claim. Verify the Origin header on upgrades too, since browsers do not apply the same-origin policy to WebSockets the way they do to fetch.

// server.js · replaces attachToServer

const { WebSocketServer } = require('ws');

// Manage the ws instance yourself so nothing is wired up before you have
// checked who is calling. With this in place, drop attachToServer().
const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', async (req, socket, head) => {
  // 1. Browsers do not enforce same-origin on WebSockets. You do.
  if (req.headers.origin !== process.env.APP_ORIGIN) return socket.destroy();

  // 2. Real identity comes from your session store, not from the page.
  const session = await getSession(req.headers.cookie);
  if (!session) return socket.destroy();

  // 3. The room number lives in the URL path, so it is a claim. Check it.
  const docId = decodeURIComponent(req.url.split('?')[0].slice(1));
  if (!(await canOpenDoc(session.userId, docId))) return socket.destroy();

  // Approved. Now hand the connection to Froala's relay.
  wss.handleUpgrade(req, socket, head, (conn) => {
    Collaborative.setupWSConnection(conn, req);
  });
});

The REST side needs the same two checks. Register the guard before your attachRoutes calls so that it runs first.

// server.js · register before attachRoutes

app.use('/collab/:docId', async (req, res, next) => {
  const session = await getSession(req.headers.cookie);
  if (!session) return res.status(401).json({ error: 'not signed in' });

  if (!(await canOpenDoc(session.userId, req.params.docId))) {
    return res.status(403).json({ error: 'no access to this document' });
  }

  // Suggesting versus editing is a server decision. collabConfig.user.role
  // only decides which buttons the browser draws.
  if (req.method !== 'GET' && session.role === 'viewer') {
    return res.status(403).json({ error: 'read only' });
  }

  req.user = session;   // the identity the rest of your code trusts
  next();
});

Let the server decide the role

Roles are the part of this that looks like authorization but is not. collabConfig.user.role takes one of three values, and each one controls which modes the editor will let a person switch into.

Role Modes allowed Starts in
editor Editing, suggesting, viewing Editing
suggester Suggesting, viewing Suggesting
viewer Viewing only Viewing

That gate lives in the browser, so it decides which buttons are drawn and nothing more. Someone who edits the value in devtools gets an editing toolbar. What stops them writing is the REST guard above, which is why the role has to make a round trip: your server decides it, injects it, and then enforces it independently.

// Server: send the role down with the page, alongside the key.
res.render('document', {
  appConfig: {
    froalaKey: process.env.FROALA_KEY,
    collabOrigin: process.env.COLLAB_ORIGIN,
    user: { id: session.userId, name: session.name, role: roleFor(session, docId) },
  },
});
// Client: react to mode changes so your own UI stays in step.
events: {
  'collab.modeChanged': function (newMode, prevMode) {
    document.body.dataset.collabMode = newMode;
    saveButton.disabled = newMode !== 'editing';
  },
},

// Switching programmatically is a no-op when the role forbids it,
// so this is safe to call without checking the role first.
editor.collaborative.setMode('suggesting');

Sanitize the HTML, twice

Version snapshots and async content are raw editor HTML, and the content endpoint returns it verbatim. Sanitization is your responsibility. Run a maintained server-side sanitizer with an allow-list of tags and attributes before you store the content, and again before you render it anywhere. The editor’s client-side options like htmlAllowedTags shape editing quality; they run in the browser and are not a security boundary.

//server.js · register before attachRoutes

const sanitizeHtml = require('sanitize-html');

const POLICY = {
  // Match this list to what your toolbar can actually produce.
  // Stricter than the editor is fine. Looser is a hole.
  allowedTags: ['p', 'br', 'strong', 'em', 'u', 's', 'h1', 'h2', 'h3',
                'ul', 'ol', 'li', 'blockquote', 'a', 'img',
                'table', 'thead', 'tbody', 'tr', 'th', 'td'],
  allowedAttributes: { a: ['href', 'title'], img: ['src', 'alt', 'width', 'height'] },
  allowedSchemes: ['http', 'https', 'mailto'],   // blocks javascript: and data:
};

app.use('/collab/:docId', (req, res, next) => {
  // AsyncSave posts the document as "body", VersionControl as "snapshot".
  if (typeof req.body?.body === 'string') {
    req.body.body = sanitizeHtml(req.body.body, POLICY);
  }
  if (typeof req.body?.snapshot === 'string') {
    req.body.snapshot = sanitizeHtml(req.body.snapshot, POLICY);
  }
  next();
});

That covers the write path. Apply the same policy again on the read path, wherever stored content gets rendered outside the editor, because rows written before this middleware existed have never been through it.

const { content } = await fetchDocument(docId);
res.render('document', { html: sanitizeHtml(content, POLICY) });

Encrypt everything in transit

Serve the page over HTTPS and the relay over wss://. If you terminate TLS at nginx or a cloud load balancer, confirm that the proxy forwards the Upgrade and Connection headers, and that its idle timeout is longer than the relay’s 30 second keepalive ping. A proxy that quietly drops upgrades produces a connection that reconnects forever and syncs nothing.

# nginx.conf

location / {
  proxy_pass http://127.0.0.1:3000;

  # Without these three lines the upgrade never reaches Node
  # and the browser reconnects forever.
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;

  # Comfortably longer than the relay's 30 second keepalive ping.
  proxy_read_timeout 120s;
}

Then move the client onto the secure scheme. An HTTPS page is not permitted to open a plain ws:// connection at all, so this is not optional once TLS is on.

collabConfig: {
  // wss, not ws, and the same host the page was served from.
  realTime: { syncUrl: 'wss://collab.example.com' },
  commentsUrl: 'https://collab.example.com/collab/my-doc/comments',
  // ...
}

Pick a database shape that matches your deployment

A single SQLite file is a perfectly respectable production choice for one server instance, and it is fast. Enable write-ahead logging once on the file so readers do not block writers, and back the file up on a schedule like any other database.

const Database = require('better-sqlite3');
// Run once before attachRoutes. journal_mode is stored in the file itself.
new Database(dbPath).pragma('journal_mode = WAL');

Scaling past one instance is where this needs thought. The relay keeps its room registry in process memory, so two Node processes behind a load balancer are two separate sets of rooms, and users in the same document land in different ones. Start with a single instance. When you outgrow it, add session affinity so every connection for a given docId reaches the same process, and move the REST stores onto a shared database that all instances can see.

#nginx.conf

upstream collab_backend {
  # The WebSocket path is exactly /{docId}, so hashing on the URI
  # lands every peer of a document on the same process.
  hash $request_uri consistent;

  server 10.0.0.11:3000;
  server 10.0.0.12:3000;
}

Decide your retention policy before launch rather than after. Versions are append-only and each snapshot holds a full copy of the document, so an autosave interval of one minute on a busy document produces a lot of rows. Choose how long snapshots live, what happens to comments and suggestions when a document is deleted, and who is allowed to delete them.

// cleanup.js · run nightly

const Database = require('better-sqlite3');
const db = new Database(dbPath);
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;

// Keep every version a person deliberately named.
// Expire the automatic ones, which is where the volume comes from.
const removed = db.prepare(
  `DELETE FROM versions
   WHERE is_named = 0 AND source = 'auto' AND created_at < ?`
).run(Date.now() - THIRTY_DAYS);

console.log(`Pruned ${removed.changes} autosaved versions`);

Guard the edges

Rate-limit both WebSocket connections per user and REST writes per document, since a client bug can otherwise turn into a write loop against your database. Validate docId against a strict character allow-list before it reaches a route, because it flows into URL paths and database keys. Keep the body limit generous enough for your largest realistic document but not unlimited. And know that the relay silently drops any single message larger than 1 MB, which is worth logging on the client if you expect users to paste very large content.

// server.js · register before attachRoutes

const rateLimit = require('express-rate-limit');

// docId flows into URL paths and database keys. Validate it early
// and reject anything that is not plainly a document name.
const DOC_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;

app.use('/collab/:docId', (req, res, next) => {
  if (!DOC_ID.test(req.params.docId)) {
    return res.status(400).json({ error: 'invalid docId' });
  }
  next();
});

// Reads are cheap. Writes are what a client bug turns into a loop.
const writes = rateLimit({
  windowMs: 60_000,
  limit: 120,
  // Budget per person per document, not per IP, so one busy office
  // behind one address does not throttle itself.
  keyGenerator: (req) => `${req.user.id}:${req.params.docId}`,
});

app.use('/collab/:docId', (req, res, next) =>
  req.method === 'GET' ? next() : writes(req, res, next)
);

 Do not hand out database errors

When something goes wrong at the storage layer, the endpoints answer with a 500 whose error field carries the underlying SQLite message. That is helpful in development and a small gift to anyone probing your application in production, because it names tables and columns. Mask it at the edge and keep the detail in your logs.

// Register before attachRoutes so it wraps the SDK's own responses.
app.use('/collab/:docId', (req, res, next) => {
  const send = res.json.bind(res);

  res.json = (body) => {
    if (res.statusCode >= 500) {
      console.error('collab %s %s', req.method, req.originalUrl, body);
      return send({ error: 'server error' });
    }
    return send(body);
  };

  next();
});

Send images to storage, not into the document

This one connects two limits that look unrelated. An image pasted or dropped into the editor without an upload target is embedded in the HTML as a data URI. That inflates every version snapshot, pushes requests against your body limit, and can produce a single sync message above the relay’s 1 MB ceiling, at which point the message is silently dropped and peers simply never see the paragraph. Configuring uploads turns the image into a short link and the problem disappears.

new FroalaEditor('#editor', {
  // Your endpoint stores the file and returns { "link": "https://..." }
  imageUploadURL: 'https://collab.example.com/upload/image',

  // Client-side hints that improve the error message. The server
  // still has to validate size and type for itself.
  imageMaxSize: 5 * 1024 * 1024,
  imageAllowedTypes: ['jpeg', 'png', 'gif', 'webp'],

  events: {
    'image.error': function (error) {
      console.warn('Image upload failed', error);
    },
  },
});

Expect restarts to disconnect everyone

Room state lives in process memory, so a deploy ends every session on that instance at once. Nothing is lost if your persistence URLs are configured and clients reconnect, but an abrupt close is a poor experience. Tell peers first, then stop accepting connections.

process.on('SIGTERM', () => {
  // 1001 is "going away". Clients reconnect after reconnectDelay.
  for (const client of wss.clients) client.close(1001, 'server restarting');

  // Stop taking new work and exit once open requests finish.
  server.close(() => process.exit(0));

  // Do not hang forever on a stuck connection.
  setTimeout(() => process.exit(1), 10000).unref();
});

Roll one instance at a time rather than all at once, and if you are running with session affinity, remember that draining an instance moves its documents to a different process. Peers reconnect into a fresh empty room, which is exactly the situation step 7 covers, so the starting content path has to be solid before you rely on rolling deploys.

Running it in a container

Two details bite here. better-sqlite3 compiles against the Node version in the image, so a mismatch between build and runtime produces a module that will not load. And a SQLite file written to the container filesystem is deleted with the container, which is a very quiet way to lose every comment on your first redeploy.

#Dockerfile
FROM node:22-bookworm-slim

# Needed only if no prebuilt binary matches this platform.
RUN apt-get update \
 && apt-get install -y --no-install-recommends python3 make g++ \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

# The database must outlive the container. Mount a real volume here.
VOLUME ["/app/data"]

EXPOSE 3000
CMD ["node", "server.js"]

On a platform with an ephemeral filesystem and no volume support, that is your signal to move the REST stores to a managed database rather than fighting the platform.

See it working before you build it

Froala’s collaborative editing demo puts two editors side by side on one document, or gives you an invite link to open in a second window. Type on one side and watch cursors, comments, suggestions, and version history appear on the other.

It is also a live illustration of the central lesson here. The demo runs on an ephemeral relay with no persistence behind it, so everything is discarded the moment the last participant closes their tab. That is the relay alone, doing precisely what it is designed to do. Steps 3 and 7 of this guide are what you add to make it survive.

Get the code

Fork this CodeSandbox to see the full server file or use it as a starting point for your own backend instead of assembling the pieces from scratch.

graphical user interface, text

How to Bind an Angular Rich Text Editor to Reactive Forms

Angular rich text editor with Reactive Forms integration using Froala

Your Angular text editor might be used to create blog posts, product descriptions, email templates, or support tickets. For that, your users need formatting options such as bold text, links, and bullet lists. A plain <textarea> won’t cover it. And FormControl doesn’t care what’s inside the string it’s holding.

Wiring a rich text editor into that setup sounds like it should be painful. For the basic case, it isn’t. The Froala Angular wrapper implements ControlValueAccessor, so you can put formControlName directly on an element with [froalaEditor] and it behaves like any other control.

What most write-ups skip is everything after that first win:

  • How do you validate content that’s actually HTML?
  • What happens when you call disable() on the control? (Not what you’d expect.)
  • Can you reuse the field across more than one form without copy-pasting config everywhere?

This article covers all three. One assumption going in: you already have angular-froala-wysiwyg installed. If not, start with the Angular integration guide for the install and module setup. This picks up right after that, and it’s specifically about reactive forms, not the ngModel / template-driven route.

Prerequisites

Before you start, make sure you have:

An Angular 19 or later project. The angular-froala-wysiwyg README states Angular 19+ bindings. Note that the package declares no peerDependencies on @angular/core, so npm won’t warn you if you’re on something older — you’ll find out at build time. Froala provides a separate legacy integration for Angular 4–9.

The Froala Angular wrapper installed. Install the wrapper with npm:

npm install angular-froala-wysiwyg

You don’t need to install froala-editor separately. The wrapper declares it as an exact direct dependency:

"dependencies": { "froala-editor": "5.4.0", "tslib": "^2.3.0" }

Installing froala-editor yourself with a different version will just create a duplicate in node_modules. The examples here were written against angular-froala-wysiwyg@5.4.0, which pulls in froala-editor@5.4.0.

The Froala stylesheets registered in angular.json:

"styles": [ "src/styles.css", 
"node_modules/froala-editor/css/froala_editor.pkgd.min.css", 
"node_modules/froala-editor/css/froala_style.min.css" ]

ReactiveFormsModule imported wherever your form lives.

A note on module imports

Angular 19+ uses standalone components by default, so the examples below follow that approach. In a standalone component, add ReactiveFormsModule, FroalaEditorModule, and FroalaViewModule to the component’s imports array.

Both Froala modules also expose a .forRoot(), and you’ll see it in a lot of older tutorials. You can skip it. As of 5.4.0, it returns an empty providers array:

public static forRoot(): ModuleWithProviders<FroalaEditorModule> {
  return { ngModule: FroalaEditorModule, providers: [] };
}

There are no providers to register, so importing the plain modules is enough. (You couldn’t put .forRoot() in a standalone imports array anyway — that array accepts components, directives, pipes, and NgModule classes, not the ModuleWithProviders object .forRoot() returns.)

If your application still uses NgModules, FroalaEditorModule and FroalaViewModule go in your NgModule’s imports, with or without .forRoot().

Step 1: Build the FormGroup

This step is not Froala-specific. It’s a regular FormGroup, and body is just a control that happens to hold an HTML string instead of plain text.

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Validators } from '@angular/forms';
import { FroalaEditorModule, FroalaViewModule } from 'angular-froala-wysiwyg';
import { richTextRequired, richTextMinLength } from './rich-text.validators';

@Component({
  selector: 'app-post-editor',
  imports: [ReactiveFormsModule, FroalaEditorModule, FroalaViewModule],
  templateUrl: './post-editor.component.html'
})
export class PostEditorComponent {
  private fb = inject(FormBuilder);

  protected submitted = false;

  protected form = this.fb.nonNullable.group({
    title: ['', Validators.required],
    body: ['', [richTextRequired, richTextMinLength(20)]]
  });

  protected editorOptions = {
    placeholderText: 'Write your post…',
    heightMin: 300,
    toolbarButtons: [
      'bold', 'italic', 'underline', 'formatOL', 'formatUL', 'insertLink'
    ],
    // Applied to the editable region Froala generates, not the host element.
    attributes: {
      'aria-labelledby': 'body-label'
    }
  };

  protected onSubmit() {
    this.submitted = true;
    if (this.form.invalid) {
      return;
    }
    console.log(this.form.getRawValue());
  }
}

Two things in there are worth explaining.

inject() sits above form rather than being a constructor parameter. With useDefineForClassFields — which Angular’s default ES2022 target enables — class fields are initialized before parameter properties are assigned. So if fb came from constructor(private fb: FormBuilder), the form initializer would read it before it exists and throw. Declaring fb as its own field with inject() sidesteps that.

fb.nonNullable.group() rather than fb.group(). By default, FormControl is nullable and reset() sets it back to null. That null is passed straight through to Froala’s html.set(). Making the group non-nullable means reset() returns each control to its initial value ('' here), which is what you want. It also gives you form.controls.body.value typed as a plain string.

Note that the value will look like <p>Hello <strong>world</strong></p>, not plain text. That’s why Validators.minLength and Validators.required aren’t used on body — more on that below.

Step 2: Bind Froala with formControlName

This is the part everyone actually comes here for. It’s almost anticlimactic.

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <label for="title">Post title</label>
  <input id="title" formControlName="title" />

  <span id="body-label">Post content</span>
  <div [froalaEditor]="editorOptions" formControlName="body"></div>

  @if (form.controls.body.invalid && (form.controls.body.dirty || submitted)) {
    <p class="error">Post content is required.</p>
  }

  <button type="submit">Publish</button>
</form>

[froalaEditor] turns the element into a Froala instance. formControlName="body" hooks it into the form the same way it would hook up a native input. There’s no glue code because the directive already implements ControlValueAccessor — writeValue, registerOnChange, and registerOnTouched are handled inside it. (A <textarea> works here too if you prefer; Froala’s own docs use a <div>.)

Three details here are deliberate.

The label is wired up through the editor options, not the host element. Froala builds its own editable region in the DOM; the element you put [froalaEditor] on is not the element the user types into. Putting aria-labelledby on the host is therefore not reliable — it may not land on the node the accessibility tree actually exposes. Froala’s attributes option applies them to the generated editable region instead, which is why the aria-labelledby lives in editorOptions above. If there’s no visible label element to point at, use the ariaLabel option with a string instead. Either way, check the result in your browser’s accessibility tree before you ship it.

The error is gated on dirty || submitted, not touched. This is a quirk worth knowing about. The directive wires registerOnTouched to Froala’s mousedown event, not to blur. So the control is marked touched the instant the user clicks into the editor — before they’ve typed anything or moved away. Gating a “this field is required” message on touched means it appears the moment they click in. The wrapper component later in this article fixes the underlying behavior; until then, dirty plus a submitted flag is the reliable gate.

The submit button isn’t disabled on form.invalid. A disabled submit button gives the user nothing to act on. Letting them submit and then showing the errors is better.

Step 3: Reading the Value Elsewhere in the Template

If you want a live preview, a character count outside the form, or anything else that reads the content, read the control directly. You do not need froalaModel for this:

<div class="preview">
  <div [froalaView]="form.controls.body.value"></div>
</div>

Because the group is non-nullable, form.controls.body.value is typed as string and needs no fallback.

If you go through form.value instead, you will need one:

<div [froalaView]="form.value.body ?? ''"></div>

A FormGroup‘s .value getter excludes any control that’s currently disabled, so with typed reactive forms the property type is string | undefined even when you know the control is enabled right now. [froalaView] expects a plain string. Reading form.controls.body.value avoids the whole issue.

What not to do

You’ll see this pattern suggested. It doesn’t work:

<!-- Don't do this -->
<div [froalaEditor] formControlName="body" [(froalaModel)]="form.value.body"></div>

form.value returns the FormGroup’s internal value snapshot. Assigning form.value.body = $event mutates that object directly, and Angular overwrites it on the next internal update — so the write silently disappears. It also creates a second, competing write path alongside formControlName.

If you genuinely need froalaModel alongside formControlName, bind it to a real component field:

<div [froalaEditor] formControlName="body" [(froalaModel)]="previewHtml"></div>

A note on update frequency

The directive syncs the model on Froala’s contentChanged event, which fires as the user types. There’s also an immediateAngularModelUpdate option, off by default, that adds a keyup listener on top of that. On longer documents, it can visibly cost you performance, because change detection re-runs on every character. Turn it on deliberately, not out of habit.

How to Validate Rich Text Content in Angular Reactive Forms

Here’s a gotcha that’s easy to miss. Validators.required doesn’t reliably catch an empty rich text editor. Froala returns HTML such as <p><br></p> when no visible content remains, and Angular’s required validator sees a non-empty string.

The obvious fix — strip all tags with a regex and check what’s left — has its own bug: a post containing only an image, a table, or an embedded video would be treated as empty. For a rich text editor, that’s not an edge case.

Parse the HTML instead, and check for both text and embedded content. These validators rely on DOMParser, which is a browser API, so they skip validation when it isn’t available rather than assuming they’re always running in a browser — see the SSR section for why:

// rich-text.validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

const EMBEDDED_CONTENT = 'img, video, audio, iframe, embed, object, table, hr';

/** DOMParser is browser-only, so these validators no-op during SSR. */
function canParseHtml(): boolean {
  return typeof DOMParser !== 'undefined';
}

function parseBody(html: unknown): HTMLElement | null {
  if (typeof html !== 'string' || html.length === 0) {
    return null;
  }
  return new DOMParser().parseFromString(html, 'text/html').body;
}

function textContentOf(body: HTMLElement | null): string {
  // &nbsp; becomes U+00A0 once parsed, so normalise it before trimming.
  return (body?.textContent ?? '').replace(/\u00a0/g, ' ').trim();
}

export function richTextRequired(
  control: AbstractControl
): ValidationErrors | null {
  if (!canParseHtml()) {
    return null; // Re-runs in the browser once the client form is created.
  }

  const body = parseBody(control.value);
  const hasText = textContentOf(body).length > 0;
  const hasEmbed = body?.querySelector(EMBEDDED_CONTENT) != null;

  return hasText || hasEmbed ? null : { required: true };
}

export function richTextMinLength(min: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!canParseHtml()) {
      return null;
    }

    const length = textContentOf(parseBody(control.value)).length;
    if (length === 0 || length >= min) {
      return null; // Let richTextRequired own the empty case.
    }
    return {
      richTextMinLength: { requiredLength: min, actualLength: length }
    };
  };
}

Both are used like any other Angular validator, as shown in Step 1:

body: ['', [richTextRequired, richTextMinLength(20)]]

richTextMinLength counts visible characters, not markup, so a 20-character minimum means 20 characters the reader can actually see. For a word count instead, split the normalized text on whitespace and check the resulting array length.

Neither validator is a substitute for validating the submitted HTML on your server. Treat both as UI affordances.

If you only need to display a character count rather than validate against it, Froala has a built-in character counter. See the Angular character-counter tutorial.

How to Track Editor Changes with valueChanges

Because body is a regular FormControl, you can subscribe to its valueChanges observable. This is useful for autosaving drafts without relying on Froala-specific events.

import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class PostEditorComponent {
  // ...fb, form, and onSubmit from Step 1...

  constructor() {
    this.form.controls.body.valueChanges
      .pipe(
        debounceTime(500),
        distinctUntilChanged(),
        takeUntilDestroyed()
      )
      .subscribe(html => this.saveDraft(html));
  }

  private saveDraft(html: string) {
    // Send the draft to your API.
  }
}

debounceTime(500) waits until changes have paused for half a second before calling saveDraft(), and distinctUntilChanged() prevents consecutive duplicate values from triggering another save.

takeUntilDestroyed() is doing real work here. Without it, the subscription outlives the component and keeps firing saves against a destroyed instance. Calling it with no arguments requires an injection context, which is why the setup lives in the constructor rather than ngOnInit. (The form field initializer runs before the constructor body, so this.form is already there.)

Updating and Resetting the Editor Through the Form

Once Froala is bound with formControlName, you can update its value with the same reactive forms APIs you use everywhere else.

To load existing content, for example, when editing a saved post:

this.form.patchValue(
  { title: post.title, body: post.bodyHtml },
  { emitEvent: false }
);

{ emitEvent: false } matters if you set up the autosave above. patchValue emits valueChanges by default, so without it, loading a post immediately fires a draft save of content you just loaded.

To reset:

this.form.reset();

Because the group was built with fb.nonNullable, this returns each control to its initial value rather than null. If you’re using a nullable group instead, pass the values explicitly:

this.form.reset({ title: '', body: '' });

Otherwise, null reaches Froala’s html.set().

Why disable() Doesn’t Disable the Editor

This is the one that catches people out.

this.form.controls.body.disable();

Angular marks the control disabled, drops it from form.value, and skips its validators. The editor itself carries on as normal — full toolbar, fully typeable.

The reason is in the wrapper. setDisabledState is the ControlValueAccessor method Angular calls when a control’s disabled state changes, and in angular-froala-wysiwyg@5.4.0 it looks like this:

setDisabledState(isDisabled: boolean): void {
}

An empty body. It exists to satisfy the interface and does nothing.

You have two options. The quick one is to reach for the editor instance and call Froala’s Edit API directly (editor.edit.off() and editor.edit.on()) whenever you toggle the control. That works, but you now have two places to keep in sync, and it’s exactly the copy-paste problem the next section solves.

Wrapping Froala in a Reusable Form Control

If you use a rich text field in more than one form — and most applications do — wrap it in a component of your own that implements ControlValueAccessor. You get one place to configure the editor, a working setDisabledState, and touched that fires on blur like every other Angular control.

// rich-text-control.component.ts
import { Component, Input, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FroalaEditorModule } from 'angular-froala-wysiwyg';

/** The manual controller emitted by the directive's (froalaInit) output. */
interface FroalaControls {
  initialize: () => void;
  destroy: () => void;
  getEditor: () => any;
}

/**
 * Run our own handler, then whatever the caller passed for the same event.
 * Froala invokes event callbacks with `this` bound to the editor instance and
 * treats a `false` return as "cancel", so both are forwarded unchanged.
 */
function chain(
  original: unknown,
  ours: () => void
): (...args: unknown[]) => unknown {
  const callerHandler = typeof original === 'function' ? original : null;

  return function (this: unknown, ...args: unknown[]) {
    ours();
    return callerHandler?.apply(this, args);
  };
}

@Component({
  selector: 'app-rich-text-control',
  imports: [FroalaEditorModule],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => RichTextControlComponent),
    multi: true
  }],
  template: `
    <div
      [froalaEditor]="mergedOptions"
      [froalaModel]="value"
      (froalaModelChange)="onModelChange($event)"
      (froalaInit)="onFroalaInit($event)">
    </div>
  `
})
export class RichTextControlComponent implements ControlValueAccessor, OnInit {
  @Input() options: Record<string, unknown> = {};

  protected value = '';
  protected mergedOptions: Record<string, unknown> = {};

  private controls?: FroalaControls;
  private isDisabled = false;

  private propagateChange: (value: string) => void = () => {};
  private propagateTouched: () => void = () => {};

  ngOnInit(): void {
    const events = (this.options['events'] as Record<string, unknown>) ?? {};

    this.mergedOptions = {
      ...this.options,
      events: {
        ...events,
        // The editor is created asynchronously, so apply any disabled state
        // that arrived before it existed.
        initialized: chain(events['initialized'], () => this.applyDisabledState()),
        // Mark touched on blur, the way a native control behaves.
        blur: chain(events['blur'], () => this.propagateTouched())
      }
    };
  }

  protected onFroalaInit(event: Object): void { 
   // Froala exposes the event as Object, so cast it to our controller type. const controls = event as FroalaControls; this.controls = controls; controls.initialize(); }

   protected onModelChange(html: string): void {
    this.value = html;
    this.propagateChange(html);
  }

  // --- ControlValueAccessor ---

  writeValue(html: string | null): void {
    this.value = html ?? '';
  }

  registerOnChange(fn: (value: string) => void): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.propagateTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
    this.applyDisabledState();
  }

  private applyDisabledState(): void {
    const editor = this.controls?.getEditor();
    if (!editor?.edit) {
      return; // Not created yet — the initialized event will re-apply this.
    }
    this.isDisabled ? editor.edit.off() : editor.edit.on();
  }
}

Three things to watch when you adapt this.

The wrapper needs initialized and blur for itself, so it chains rather than replaces. Spreading the caller’s events and then assigning those two keys would silently discard any handler passed in through [options] — a nasty surprise in a component whose whole job is to be reused. chain() runs the wrapper’s handler first, then the caller’s, preserving Froala’s this binding and return value. If you add more events to the wrapper later, route them through chain() too.

Binding (froalaInit) changes the directive’s behaviour. It stops creating the editor for you and hands you a controller instead. If you bind the output and never call controls.initialize(), no editor appears. The directive types the (froalaInit) event as Object, so the handler casts it to the local FroalaControls interface before using initialize() and getEditor().

The editor is loaded through a dynamic import(). getEditor() returns null until initialization finishes, so applyDisabledState() bails out safely and gets called again from the initialized event.

Usage is then the same as any built-in control:

<app-rich-text-control
  formControlName="body"
  [options]="editorOptions" />

And this now does what you’d expect:

this.form.controls.body.disable();

Sanitize the HTML Before You Store or Render It

The editor’s output is user-supplied HTML, and the [froalaView] directive assigns it straight to innerHTML:

@Input() set froalaView(content: string) {
  this._element.innerHTML = content;
}

That’s a direct assignment, which bypasses Angular’s DomSanitizer—unlike Angular’s own [innerHTML] binding, which sanitizes. Whatever the editor produces is rendered as-is.

Render unsanitized HTML only when the value is known to be trusted. Content loaded from storage, an API, a collaboration session, or another user should be treated as untrusted — including in a preview pane. The preview in Step 3 is safe only because that specific value came from the editor the current user is typing into right now; swap in patchValue() from a saved post and it isn’t.

Sanitize on the server before you persist, using a maintained HTML sanitizer with an allowlist that matches the toolbar you’ve configured. Client-side sanitizing is a nice-to-have; server-side is the one that counts, because your API is what an attacker will call.

A Note on SSR

Reactive forms are server-safe, and as of 5.4.0 the Froala directive handles SSR itself. It guards initialization with isPlatformBrowser and loads froala-editor through a dynamic import(), so the editor package never executes during server rendering. You don’t need to gate the component.

What still needs care is your own browser-dependent code. The validators above use DOMParser, which doesn’t exist on the server, which is why they check for it and return null when it’s missing. A control that skips validation during server rendering is validated again as soon as the client bootstraps and builds its own form, so nothing reaches the user unvalidated.

If you’d rather validate during server rendering too, swap canParseHtml() for a parser that runs in both environments and give the validators a Node-side implementation. Either way, validate the submitted HTML on your server as well — client-side validators are a UX affordance, not a gate.

For anything that must run after the DOM exists, current Angular versions provide afterNextRender() and afterEveryRender(); neither runs during server rendering. For a complete setup, see the Froala Angular SSR tutorial.

FAQ

Does Froala’s Angular editor support reactive forms?

Yes. Add formControlName to the element using [froalaEditor]. The directive implements ControlValueAccessor, so no adapter code is needed for basic usage.

Why does Validators.required still pass on an empty editor?

Froala returns non-empty HTML, such as an empty paragraph, when no visible content remains, and Validators.required only checks that the string isn’t empty. Use a validator that parses the HTML and checks for real text or embedded content.

Why doesn’t disable() disable the editor?

Because setDisabledState in angular-froala-wysiwyg is an empty method. Wrap the editor in your own ControlValueAccessor component that calls Froala’s edit.off() / edit.on(), as shown above.

Why does my “required” error appear as soon as I click into the editor?

The directive marks the control as touched on mousedown rather than on blur. Gate the message on dirty and a submitted flag, or use the wrapper component, which marks touched on the editor’s blur event.

Can I use both formControlName and [(froalaModel)] together?

Yes, but bind froalaModel to a component field, not to form.value.body. Writing to form.value mutates a snapshot Angular replaces on its next update.

Do I need to install froala-editor separately?

No. angular-froala-wysiwyg declares it as an exact direct dependency and installs it for you.

Wrapping Up

You don’t need any adapter code to bind the Froala rich text editor to Angular reactive forms for the basic case. Add formControlName to the element using [froalaEditor], and the editor behaves like any other control: patchValue() loads content, valueChanges reports edits, reset() clears it.

Two things are different from a plain text input, and both come down to the same cause — the value is HTML, and the editor is a third-party widget rather than a native element. Validation has to account for markup, so Validators.required and Validators.minLength need HTML-aware replacements. And disable() doesn’t reach the editor, because the wrapper’s setDisabledState is a no-op.

If you’re using a rich text field in more than one form, the wrapper component above is worth the extra file. It gives you one place to configure the editor, a disable() that works, and touched-on-blur semantics for every form that uses it.

Get the complete example from our GitHub repo. For more configuration options and events, see the Angular integration documentation. If you haven’t installed Froala yet, you can start a free trial and try the examples in your own project. For additional Angular resources, visit the Angular Rich Text Editor page.

graphical user interface, text

Stream AI Answers Into Froala Through Your Own Server

Stream AI Answers

You have probably used an AI chat app where the answer types itself out one word at a time. It feels quick and alive. Now compare that to an app where you ask a question, stare at a spinner for nine seconds, and then the whole answer drops in at once. Same answer, same speed, completely different feeling.

Froala’s AI Chat panel has supported typing-out functionality since v5.4.0. This tutorial focuses on building a production-ready version that streams answers word by word, secures your API key on your server, and includes a Cancel button that genuinely cancels all the way back to the provider.

AI Stream

What we are building

Three pieces talking to each other:

   Browser                    Your Node server              AI provider
  (Froala AI Chat)                (the proxy)                  (Groq)
        │                              │                          │
        │  POST /ai/stream             │                          │
        ├─────────────────────────────►│  POST /chat/completions  │
        │                              ├─────────────────────────►│
        │                              │            🔑 key added   │
        │   data: {"delta":"Hello"}    │   data: {...}            │
        │◄─────────────────────────────┤◄─────────────────────────┤
        │   data: {"delta":" there"}   │   data: {...}            │
        │◄─────────────────────────────┤◄─────────────────────────┤
        │   data: {"done":true}        │                          │
        │◄─────────────────────────────┤                          │

By the end you will have:

  • A working server.js that streams and keeps your key safe
  • A working index.html with a real Froala editor in it
  • A Cancel button that stops the AI provider, not just the display
  • Timeouts that behave sensibly, including behind a reverse proxy like nginx
  • A practice mode so you can test all of this without an API key at all

This tutorial uses Groq because it is fast, free to start, and its API is OpenAI-compatible, meaning the exact same code works for OpenAI by changing one URL. There is a section near the end on swapping to Gemini or Claude.

Step 1: Set up the project

Open your terminal and run these four commands one at a time:

mkdir froala-ai-streaming

cd froala-ai-streaming

npm init -y

npm install express cors

What each one does:

  • mkdir makes a new folder, cd moves you into it. Everything from here happens inside this folder.
  • npm init -y creates a package.json file. Think of it as the project’s ID card: it records which packages your project uses. The -y just says “yes” to all the setup questions so you are not prompted.
  • npm install express cors downloads two small packages:
    • express is a tiny web server. It handles the boring parts of listening for requests so you can write the interesting parts.
    • cors grants your web page permission to talk to your server. Browsers block cross-origin requests by default as a security measure, and cors is how you say “this one is fine.”

Versions used when writing this: express 5.2.1, cors 2.8.6, Node 22. The code also works on Node 18+ and express 4.

Step 2: Understand the format before writing any code

One concept first, because everything after it becomes obvious once you have this.

When an AI provider streams an answer, it does not send you a JSON object. It sends a long response that dribbles in over time, made of lines that look like this:

data: {"choices":[{"delta":{"content":"Hello"}}]}

data: {"choices":[{"delta":{"content":" there"}}]}

data: [DONE]

This is called Server-Sent Events, or SSE. Two things define it:

  1. Every message starts with data: and ends with a blank line. The blank line is the “message over” marker, and it is not optional.
  2. The connection stays open the whole time. Normal HTTP is a question and an answer. SSE is more like a one-way radio broadcast: the server keeps talking, and the listener keeps listening until the server signs off.

Our server is going to do two SSE jobs at once:

  • Read the provider’s SSE stream as it arrives
  • Write its own, much simpler SSE stream out to the browser

Why translate instead of just forwarding?

You might reasonably ask why we do not just pipe the provider’s messages straight through. The answer is that it would leak the provider’s shape into your frontend. Your browser code would have to know that Groq puts text at choices[0].delta.content. The day you switch to Gemini, where it lives at candidates[0].content.parts[0].text, you would have to change your frontend too, redeploy it, and re-test it.

By translating on the server, the browser only ever sees your format. Switching providers becomes a change to one file that your frontend never notices.

Our format has exactly three kinds of message:

data: {"delta":"Hello"}                     ← a piece of the answer
data: {"done":true,"webSearchUsed":false}   ← the answer is finished
data: {"error":"AI request failed"}         ← something went wrong

Simple enough to hold in your head. That is the point.

Step 3: Build the server, piece by piece

I am going to show you the server in four chunks so you can see what each part is for, then give you the complete file to copy.

3.1 The setup and the SSE headers

const express = require('express');
const cors = require('cors');
const path = require('path');

const app = express();

app.use(cors());                                 // let a browser page call this server
app.use(express.json({ limit: '25mb' }));        // parse JSON bodies; file context can be large
app.use(express.static(path.join(__dirname)));   // also serve index.html from this same folder

app.post('/ai/stream', async (req, res) => {
  // Tell the browser: this is a stream, not a normal one-and-done response
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

Why these headers matter:

  • The text/event-stream is the official SSE content type, informing browsers and proxies that the response will be delivered in fragments.
  • no-transform asks intermediaries not to “helpfully” rewrite or compress the body. Compression is the enemy of streaming, because compressors like to buffer.
  • X-Accel-Buffering: no is a specific instruction to nginx meaning “do not hold this back.” We will come back to this in the timeouts section, because it is the single most common reason streaming works locally and then breaks in production.
  • res.flushHeaders() sends the headers immediately rather than waiting for the first chunk of body. This is what makes the connection open right away.

Notice express.static too. It serves index.html from the same folder, so your page and your API live at the same address. That sidesteps a whole category of beginner CORS pain, and it also matches how you would deploy for real.

3.2 The abort chain (the part that saves you money)

  const send = (payload) => {
    if (!res.writableEnded) res.write(`data: ${JSON.stringify(payload)}\n\n`);
  };

  const upstream = new AbortController();
  let finished = false;

  res.on('close', () => {
    if (!finished) {
      console.log('Client hung up — cancelling the provider request.');
      upstream.abort();
    }
  });

An AbortController is a remote stop button for an in-flight request. You create one, hand its .signal to fetch, and later calling .abort() cancels that fetch wherever it has got to.

res.on('close') fires when the browser hangs up. That happens when the user clicks Cancel in the chat panel, closes the tab, or their wifi drops. Without this listener, your server would happily keep receiving an answer nobody will ever read, and your provider would keep billing you for every token of it.

The finished flag exists because close also fires after a completely normal, successful ending. Without the flag you would fire a pointless abort every single time.

The send helper does two jobs: it formats valid SSE (note the two \n characters, which create that mandatory blank line), and it checks writableEnded so you never try to write to a connection that is already gone.

3.3 The heartbeat and the idle watchdog

  const IDLE_TIMEOUT_MS = 30000;
  const HEARTBEAT_MS = 15000;

  // Send a tiny "still here" comment so idle-timeout proxies don't kill us
  const heartbeat = setInterval(() => {
    if (!res.writableEnded) res.write(': keep-alive\n\n');
  }, HEARTBEAT_MS);

  // A kitchen timer we reset every time we hear from the provider
  let idleTimer = null;
  let timedOut = false;
  const touch = () => {
    clearTimeout(idleTimer);
    idleTimer = setTimeout(() => {
      timedOut = true;
      upstream.abort();
    }, IDLE_TIMEOUT_MS);
  };

  const cleanup = () => {
    clearInterval(heartbeat);
    clearTimeout(idleTimer);
  };

Two different problems, two different tools.

The heartbeat mechanism addresses connection timeouts that occur when infrastructure (e.g., nginx, load balancers, Cloudflare) terminates quiet connections. Models sometimes think for 10–20 seconds before producing a single token. To a load balancer, thinking looks identical to being dead. So every 15 seconds we write : keep-alive. A line starting with : is an SSE comment. This comment counts as traffic, and is ignored by the browser because it does not start with data:.

The idle watchdog solves the opposite problem: the provider accepts your request and then goes silent forever. Node’s built-in fetch has no default timeout, so without this your server would wait indefinitely, holding a connection open for nothing. So we set a 30-second kitchen timer, and every time a chunk arrives we reset it. The timer only fires if the provider genuinely goes quiet.

Note it is an idle timeout, not a total one. A three-minute answer is fine as long as something arrives every 30 seconds. A total timeout would cut off long answers for no good reason.

cleanup() clears both timers. Forgetting this is a real memory leak: an interval that keeps running after the request is gone.

3.4 Talking to the provider and parsing its stream

  const providerResponse = await fetch(PROVIDER_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      model: req.body.model || DEFAULT_MODEL,
      messages: [{ role: 'user', content: req.body.prompt }],
      stream: true                       // ← the flag that turns streaming on
    }),
    signal: upstream.signal              // ← our stop button, wired in
  });

  const reader = providerResponse.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let buffer = '';
  let done = false;

  while (!done) {
    const result = await reader.read();
    done = result.done;
    touch();                             // ← we heard something; reset the kitchen timer

    buffer += decoder.decode(result.value || new Uint8Array(), { stream: !done });

    const lines = buffer.split('\n');
    buffer = lines.pop();                // ⚠️ the last line may be cut in half — keep it

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith('data:') || trimmed === 'data: [DONE]') continue;

      let parsed;
      try { parsed = JSON.parse(trimmed.slice(5)); } catch { continue; }

      const delta = parsed.choices?.[0]?.delta?.content;
      if (delta) send({ delta });        // forward this piece to the browser right now
    }
  }

This buffer = lines.pop() line deserves a moment, because it is where almost every first attempt breaks.

Data arrives in chunks of whatever size the network felt like, which has nothing to do with where the lines end. You will genuinely receive things like:

data: {"choices":[{"delta":{"content":"Hel

…with the rest arriving 300 milliseconds later. Call JSON.parse on that and it throws.

So: split on newlines, process every complete line, and pop the last one back into buffer because you cannot be sure it finished. Next time round, the remainder gets glued onto it.

Analogy: it is like reading a book where a sentence runs across a page break. You do not try to make sense of the half-sentence at the bottom. You hold onto it until you turn the page.

getReader() is how you read a response body in pieces instead of waiting for all of it. TextDecoder turns raw bytes into text, and { stream: true } tells it “more bytes are coming, so if these end mid-character, hang on to the fragment.” Same idea as the line buffer, one level lower.

3.5 The complete server file

Create a file called server.js in your project folder and paste this in. This is the whole thing, including practice mode and the plain non-streaming route we will discuss in Step 4.

// server.js — a small proxy between Froala and your AI provider.

const express = require('express');
const cors = require('cors');
const path = require('path');

const app = express();

app.use(cors());                                // let a browser page call this server
app.use(express.json({ limit: '25mb' }));       // file context can make request bodies large
app.use(express.static(path.join(__dirname)));  // serve index.html from this same folder

// 🔑 Your key lives here, on the server. Never in the browser.
// Leave it unset to run in practice mode (fake answers, no key needed).
const API_KEY = process.env.GROQ_API_KEY || '';

const PROVIDER_URL = 'https://api.groq.com/openai/v1/chat/completions';
const DEFAULT_MODEL = 'llama-3.1-8b-instant';

const IDLE_TIMEOUT_MS = 30000;  // give up if the provider goes quiet this long
const HEARTBEAT_MS = 15000;     // how often to prove we're still alive

const PRACTICE_MODE = !API_KEY;

// A sleep that wakes up early if the request gets cancelled.
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      const error = new Error('Aborted');
      error.name = 'AbortError';
      reject(error);
    }, { once: true });
  });
}

// Fake provider: emits one word at a time so you can test with no API key.
async function practiceStream(signal, onDelta) {
  const words = ('Practice mode is on, so this answer is coming from your own ' +
    'server instead of a real AI provider. Every word you are reading arrived ' +
    'as a separate little message. Try pressing Cancel while this is still ' +
    'typing and watch your terminal.').split(' ');

  for (const word of words) {
    await sleep(120, signal);   // rejects immediately if the user cancelled
    onDelta(word + ' ');
  }
}

// ─── The streaming route: used by the AI Chat panel ──────────────────────────
app.post('/ai/stream', async (req, res) => {
  // 1. Tell everyone in the chain this is a stream
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');   // stops nginx buffering the stream
  res.flushHeaders();                          // open the connection right now

  // Writes one valid SSE message. The blank line at the end is required.
  const send = (payload) => {
    if (!res.writableEnded) res.write(`data: ${JSON.stringify(payload)}\n\n`);
  };

  // 2. Our stop button for the request going out to the provider
  const upstream = new AbortController();
  let finished = false;
  let timedOut = false;

  // 3. Heartbeat: proves we're alive so proxies don't kill an idle connection
  const heartbeat = setInterval(() => {
    if (!res.writableEnded) res.write(': keep-alive\n\n');
  }, HEARTBEAT_MS);

  // 4. Idle watchdog: a kitchen timer we reset each time the provider speaks
  let idleTimer = null;
  const touch = () => {
    clearTimeout(idleTimer);
    idleTimer = setTimeout(() => {
      timedOut = true;
      upstream.abort();
    }, IDLE_TIMEOUT_MS);
  };

  const cleanup = () => {
    clearInterval(heartbeat);
    clearTimeout(idleTimer);
  };

  // 5. If the browser hangs up (Cancel, closed tab, dropped wifi), stop the provider
  res.on('close', () => {
    if (!finished) {
      console.log('Client hung up — cancelling the provider request.');
      upstream.abort();
    }
    cleanup();
  });

  touch();  // start the watchdog

  try {
    // ---- Practice mode: no key needed, fake words ----
    if (PRACTICE_MODE) {
      await practiceStream(upstream.signal, (delta) => {
        touch();
        send({ delta });
      });
      send({ done: true, webSearchUsed: false });
      finished = true;
      cleanup();
      return res.end();
    }

    // ---- Real mode: ask the provider for a streaming response ----
    const providerResponse = await fetch(PROVIDER_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`
      },
      body: JSON.stringify({
        model: req.body.model || DEFAULT_MODEL,
        messages: [{ role: 'user', content: req.body.prompt }],
        stream: true,   // ← the one flag that turns streaming on
        ...(req.body.webSearchEnabled ? { tools: [{ type: 'browser_search' }] } : {})
      }),
      signal: upstream.signal   // ← wire the stop button into the outgoing request
    });

    if (!providerResponse.ok) {
      const detail = await providerResponse.text();
      console.error('Provider error:', providerResponse.status, detail);
      send({ error: 'The AI provider rejected the request.' });
      finished = true;
      cleanup();
      return res.end();
    }

    // ---- Read the provider's stream piece by piece ----
    const reader = providerResponse.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let buffer = '';
    let webSearchUsed = false;
    let done = false;

    while (!done) {
      const result = await reader.read();
      done = result.done;
      touch();   // we heard something — reset the watchdog

      // Bytes → text, glued onto whatever was left over last round
      buffer += decoder.decode(result.value || new Uint8Array(), { stream: !done });

      const lines = buffer.split('\n');
      buffer = lines.pop();   // ⚠️ last line may be cut in half — save it for next round

      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed.startsWith('data:') || trimmed === 'data: [DONE]') continue;

        let parsed;
        try {
          parsed = JSON.parse(trimmed.slice(5));
        } catch {
          continue;   // a malformed line isn't worth killing the whole stream over
        }

        const choice = parsed.choices?.[0];

        // Did the model actually run a web search? Only it can tell us.
        const executedTools = choice?.delta?.executed_tools || choice?.message?.executed_tools;
        if (executedTools && executedTools.length) webSearchUsed = true;

        const delta = choice?.delta?.content;
        if (delta) send({ delta });   // forward this piece to the browser immediately
      }
    }

    // 6. Sign off, and report whether a search actually happened
    send({ done: true, webSearchUsed });
    finished = true;
    cleanup();
    res.end();

  } catch (error) {
    cleanup();

    if (timedOut) {
      console.error('Provider went quiet for too long — gave up.');
      send({ error: 'The AI provider stopped responding.' });
      finished = true;
      return res.end();
    }

    // The user cancelled. The connection is already gone; nothing to report.
    if (error.name === 'AbortError') return;

    console.error(error);
    send({ error: 'AI request failed.' });
    finished = true;
    res.end();
  }
});

// ─── The plain route: used by every other AI feature ─────────────────────────
app.post('/ai', async (req, res) => {
  try {
    if (PRACTICE_MODE) {
      return res.json({ answer: 'Practice mode: this is a fake non-streaming answer.' });
    }

    const providerResponse = await fetch(PROVIDER_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`
      },
      body: JSON.stringify({
        model: req.body.model || DEFAULT_MODEL,
        messages: [{ role: 'user', content: req.body.prompt }]
        // note: no `stream: true` here — we want the whole answer at once
      }),
      signal: AbortSignal.timeout(IDLE_TIMEOUT_MS)   // built-in one-line timeout
    });

    const data = await providerResponse.json();
    res.json({ answer: data.choices?.[0]?.message?.content || '' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'AI request failed.' });
  }
});

app.listen(3000, () => {
  console.log('AI server running on http://localhost:3000');
  if (PRACTICE_MODE) {
    console.log('PRACTICE MODE: no GROQ_API_KEY found, serving fake answers.');
  }
});

Step 4: Why there are two routes

You may have noticed the second, much shorter route at the bottom. That is deliberate, and skipping it causes a confusing bug later.

The AI Chat panel streams. But the AI Assist plugin bundles several other features, and they all share the same aiAssistRequest connection:

Feature Streams?
AI Chat panel Yes
AI Assist prompt popup No
AI Shortcuts (tone, translate) No
Improve Writing No
Inline suggestions (ghost text) No

Froala tells you which kind it wants by whether it passes a third argument. One function handles both cases, so your server needs both routes to send it to. Build only the streaming one and Improve Writing will quietly fail.

Heads up about inline suggestions. They are on by default (inlineSuggestions), and they fire a request every time the user pauses typing. That is a lot of small hits on your proxy. After the demo phase, consider routing these requests, identifiable by an autoComplete field, to a more cost-effective, smaller model.

Step 5: Start the server (no API key required yet)

node server.js

You should see:

AI server running on http://localhost:3000
PRACTICE MODE: no GROQ_API_KEY found, serving fake answers.
node start

Practice mode is on because you have not set a key yet, and that is exactly what we want right now. It emits fake words on a timer, one every 120ms. That is enough to prove the streaming plumbing, the Cancel button, and the abort chain all work, before you spend a single token or debug a single API error. Once everything works, you add the key and nothing else changes.

Step 6: Build the editor page

Create index.html in the same folder as server.js.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Froala AI Chat — streaming through your own server</title>

  <!-- Froala's stylesheet -->
  <link href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css" rel="stylesheet">

  <style>
    body { font-family: system-ui, sans-serif; max-width: 1000px; margin: 40px auto; padding: 0 16px; }
  </style>
</head>
<body>

  <h1>Froala AI Chat (streaming proxy demo)</h1>

  <!-- Froala replaces this div with the editor -->
  <div id="editor">
    <p>Open the AI Chat panel and ask it something. The answer should type itself out.</p>
  </div>

  <!-- The "pkgd" bundle includes every plugin, AI Assist included -->
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>

  <script>
    // This one function is Froala's entire connection to your AI.
    // Froala calls it every time a user sends a message.
    async function aiHandler(request, signal, onChunk) {
      // Froala passes a 3rd argument ONLY when it wants a streaming answer.
      // If it's missing, it wants one complete answer instead.
      const wantsStreaming = typeof onChunk === 'function';

      const response = await fetch(wantsStreaming ? '/ai/stream' : '/ai', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          prompt: request.prompt,                      // question + any attached context
          model: request.model,                        // only set if you configured aiChatModels
          webSearchEnabled: request.webSearchEnabled   // true if the globe toggle was on
        }),
        signal   // ← hands Froala's Cancel button control of this request
      });

      if (!response.ok) {
        throw new Error('The AI server returned ' + response.status);
      }

      // ---- The simple path: one answer, no streaming ----
      if (!wantsStreaming) {
        const data = await response.json();
        return data.answer;
      }

      // ---- The streaming path: read our server's SSE messages as they arrive ----
      const reader = response.body.getReader();
      const decoder = new TextDecoder('utf-8');
      let buffer = '';
      let answer = '';
      let webSearchUsed = false;
      let done = false;

      while (!done) {
        const result = await reader.read();
        done = result.done;

        buffer += decoder.decode(result.value || new Uint8Array(), { stream: !done });

        const lines = buffer.split('\n');
        buffer = lines.pop();   // same half-a-line problem as on the server

        for (const line of lines) {
          const trimmed = line.trim();
          // Skips our ": keep-alive" heartbeats automatically — they aren't "data:"
          if (!trimmed.startsWith('data:')) continue;

          let payload;
          try {
            payload = JSON.parse(trimmed.slice(5));
          } catch {
            continue;
          }

          if (payload.error) throw new Error(payload.error);

          if (payload.delta) {
            answer += payload.delta;   // keep our own copy of the full answer
            onChunk(payload.delta);    // 👈 this is what paints text into the panel
          }

          if (payload.done) webSearchUsed = !!payload.webSearchUsed;
        }
      }

      // Froala needs the complete answer back at the end, even when streaming.
      return { answer: answer.trim(), webSearchUsed };
    }

    new FroalaEditor('#editor', {
      // 1️⃣ REQUIRED. Without this, every AI feature stays off and no icons appear.
      aiSupplementalTermsAccepted: true,

      // 2️⃣ REQUIRED. Tells Froala how to reach an AI.
      aiAssistRequest: aiHandler,

      // Streaming is on by default. Flip to false to test the plain path.
      aiChatStreamResponse: true,

      // How long the browser waits before giving up. Default is 60000 (60s).
      aiChatRequestTimeoutMs: 120000,

      toolbarButtons: [
        'bold', 'italic', 'underline', '|',
        'aiChatAssistant',   // opens the AI Chat side panel
        'aiAssist',          // opens the one-off prompt popup
        'aiShortCuts'        // tone & translate dropdown
      ]
    });
  </script>
</body>
</html>

The two settings that matter most are aiSupplementalTermsAccepted and aiAssistRequest. The AI icons will not appear on the toolbar until both settings are configured.

Step 7: The three things worth understanding in that handler

Why we check for a third argument. Froala decides whether it wants a stream and tells you by passing onChunk. Your function does not choose; it responds. This is why one handler serves the chat panel, the prompt popup, and Improve Writing with no extra configuration.

Why we still return the full answer at the end. onChunk paints text on screen as it arrives, but Froala needs the finished answer for everything else: the Copy button, “Apply to document”, and getChatHistory(). So you accumulate pieces in answer as you go and hand the whole thing back at the end. Forgetting this produces a genuinely baffling bug: the text appears perfectly, then Apply inserts nothing.

Why not EventSource? If you have read about SSE before, you may know browsers have a built-in EventSource class for exactly this. We cannot use it: EventSource only makes GET requests and cannot send a request body. A prompt with document context attached can be many thousands of characters, so it has to go in a POST body. Reading the stream manually with getReader() is the standard workaround, and it is barely more code.

Understanding abort propagation

This is the part that separates a demo from something real, so let us trace it end to end.

When your user clicks Cancel, four things have to happen in order, and each one is a link in a chain:

1. User clicks Cancel in the chat panel
        ↓  Froala aborts the `signal` it gave your handler
2. Your browser fetch() is cancelled
        ↓  the TCP connection to your server closes
3. Your server's res.on('close') fires
        ↓  you call upstream.abort()
4. Your server's fetch to the provider is cancelled
        ↓  the provider stops generating (and stops charging you)

Break any link and the ones after it never happen.

There are exactly two places this chain usually breaks, and both are one-line mistakes:

Link 2 breaks if you forget to pass signal to fetch. This is the most common one. The panel stops showing text, because Froala stops listening, but your browser is still downloading and your server is still streaming. It looks fixed. Nothing was cancelled.

// ❌ Cancel does nothing beyond the UI
const response = await fetch('/ai/stream', { method: 'POST', body });

// ✅ Cancel propagates
const response = await fetch('/ai/stream', { method: 'POST', body, signal });

Link 4 breaks if you forget signal: upstream.signal on the server’s fetch. Then your server correctly notices the hangup but has no way to act on it, and the provider keeps generating a full answer into the void. You are billed for every token.

The timeout is the same thing wearing a different hat

Here is a detail that surprises people: when Froala’s aiChatRequestTimeoutMs expires, it aborts the same signal. From your handler’s point of view, a timeout and a user cancellation are identical events.

That is good news. It means you do not write separate timeout-handling code. If your abort chain works, timeouts clean up the whole chain for free.

Why the AbortError gets swallowed on purpose

Look at this line in the server’s catch block:

if (error.name === 'AbortError') return;

When the browser hangs up, the connection is already gone. Trying to write an error message to it would either throw or silently do nothing. And there is nobody left to read it. So we return quietly. This is the one case where swallowing an error is correct rather than lazy.

Understanding timeouts under a proxy

Locally, everything works. Then you deploy behind nginx or a load balancer and answers start cutting off at suspiciously round numbers like exactly 60 seconds. Here is why.

There are at least three timeout layers, and the shortest one always wins.

# Layer Who controls it Typical default
1 Browser → your server You, via aiChatRequestTimeoutMs 60000 (60s)
2 Anything in between (nginx, load balancer, CDN) Your infrastructure Often 60s idle
3 Your server → the provider You, via the idle watchdog we built None by default in Node

Layer 3 is the one nobody remembers. Node’s built-in fetch has no timeout at all unless you give it one. That is why we added the idle watchdog, and why the plain /ai route uses AbortSignal.timeout().

Typical proxy defaults to check

These are the usual values. Do not trust the table; check your own config, because they change between versions and providers.

Platform Setting to look at Commonly defaults to
nginx proxy_read_timeout, proxy_buffering 60s, buffering on
AWS Application Load Balancer Idle timeout 60s
Heroku Router timeout 55s
Cloudflare Proxy read timeout ~100s

The two fixes

Fix 1: the heartbeat handles idle timeouts. Every one of those settings is an idle timeout: it kills the connection when nothing has moved for N seconds. Our : keep-alive comment every 15 seconds means the connection is never idle, even while the model is thinking. This is why the heartbeat exists.

Fix 2: turn off buffering, or you get no streaming at all. This one produces the most confusing symptom in the whole tutorial: your server streams perfectly, and the browser shows nothing until the very end. Nothing is broken. Something in the middle is collecting the whole response before passing it on, exactly as it would for a normal web page.

Three things buffer, and you need to defeat all of them:

# nginx: for your streaming route
location /ai/stream {
    proxy_pass http://localhost:3000;
    proxy_buffering off;          # don't collect the response before forwarding
    proxy_read_timeout 300s;      # allow long answers
    proxy_set_header Connection '';
    proxy_http_version 1.1;
}

The X-Accel-Buffering: no header we set in Step 3 does the same job and travels with the response, so it keeps working even if you do not control the nginx config. Belt and braces set both.

The third buffer is in your own code:

// ❌ Do NOT do this — compression buffers, and buffering defeats streaming
app.use(compression());

If you need compression() for the rest of your app, skip it for the streaming route:

app.use(compression({
  filter: (req, res) => res.getHeader('Content-Type') !== 'text/event-stream'
}));

Choosing your numbers

A sensible arrangement, from the inside out:

  • Idle watchdog (server → provider): 30s. Long enough for a model to think, short enough to notice a dead provider.
  • Proxy read timeout: 300s. Generous, because the heartbeat keeps the connection busy anyway.
  • aiChatRequestTimeoutMs: 120000. Double the default. Long answers with document context genuinely take a while.

Setting aiChatRequestTimeoutMs: 0 disables the browser timeout entirely.

Testing it

Open your browser to http://localhost:3000

Test 1: does it stream?

  1. Confirm node server.js is still running.
  2. Click the AI Chat icon in the toolbar. The panel slides in from the right.
  3. Type anything and press Enter.

You should see words appear one at a time. In practice mode you will get the canned message about practice mode, arriving word by word. This proves the entire pipeline works.

Test 2: does Cancel actually cancel?

  1. Send another message.
  2. While it is still typing, click the Cancel button. The send arrow turns into a stop control while a response is generating.
  3. Watch your server’s terminal.

You should see:

Client hung up — cancelling the provider request.

That line is your proof. The abort travelled from a button click in the browser all the way to your server, which then cancelled the request going out to the provider. If that message does not appear, revisit the abort propagation section, you have a broken link.

Test 3: does the heartbeat work?

Temporarily bump HEARTBEAT_MS down to 2000 and restart the server. Open DevTools → Network tab, send a message, click the /ai/stream request, and look at the Response or EventStream tab. You should see : keep-alive lines interleaved with the data. Those are what keep a proxy from hanging up on you. Set it back to 15000 afterwards.

Test 4: switch on a real AI

Now that everything works, add your key. Stop the server with Ctrl + C, then:

# macOS / Linux
GROQ_API_KEY=your-real-key-here node server.js

# Windows PowerShell
$env:GROQ_API_KEY="your-real-key-here"; node server.js

The practice-mode line should disappear from the startup output. Ask a real question, and repeat Test 2. Cancelling now saves you real money.

Going to production

The code above is deliberately minimal so the streaming stays visible. Before it goes anywhere real, five changes:

1. Move the key to an environment variable properly. Create a .env file, add GROQ_API_KEY=your-real-key, add .env to your .gitignore, and load it with the dotenv package. A key committed to git stays readable in the history even after you delete the line, so treat any key that has ever been committed as compromised and rotate it with the provider.

2. Lock down CORS. app.use(cors()) currently allows any website on the internet to use your AI endpoint at your expense:

app.use(cors({ origin: 'https://yourapp.com' }));

3. Add authentication and rate limiting. Your /ai/stream route is an open, unmetered door to a paid API. At minimum, check a session token on every request and cap how many calls one user can make per minute. Remember inline suggestions fire on every typing pause, so set that cap thoughtfully.

4. Remember what is in the prompt. request.prompt contains whatever the user attached as context, which can be the entire document. If those documents might hold sensitive customer data, your proxy is the right place to redact, log, or refuse it. It is the last point you control before the data reaches a third party.

5. Cap concurrent streams. Every open stream holds a connection on your server and an interval timer. Fine for ten users, less fine for ten thousand. Measure before you assume.

What you have built

A streaming AI Chat panel where the answer appears as it is written, the Cancel button genuinely cancels all the way back to the provider, timeouts behave sensibly at all three layers, and your API key never touches the browser.

That is the same architecture behind every serious AI editing feature you have used. It is not a toy version.

Next

You now have a working proxy. The next step depends on what you want to build:

  • Add more AI features (tone, translate, improve writing): See the AI Assist Plugin reference.
  • Switch providers (OpenAI, Claude, Gemini): The AI Chat Quick Start has parsing code for each one. Only your server changes; the browser stays the same.
  • Deploy to production: Work through the five items in Going to production above before you do. The proxy is the last point you control before data reaches a third party.

graphical user interface, text

Reading and Writing Markdown in Froala Editor

Reading and Writing Markdown in Froala Editor

Froala v5.4.0 adds getMarkdown() and setMarkdown(), so Markdown can be the format your application stores rather than something you convert once on the way out. Before you wire them up, it helps to understand how Froala keeps Markdown content separate from rich text content. That design protects your users’ work, and it explains exactly what each of these two methods reads, writes, and returns.

Key Takeaways

  • Markdown mode and Rich Text mode each hold their own content, so switching between them is never destructive.
  • getMarkdown() and setMarkdown() work only with the Markdown side of the editor.
  • A null return from setMarkdown() means the value was cached, not that the call failed.
  • getMarkdown() fully replaces the older custom export button built on selection.text().

How Froala Separates Markdown Content From Rich Text Content

Froala gives you two modes, and each one holds its own content:

  • Rich Text mode, the familiar WYSIWYG experience where users type and format with the toolbar.
  • Markdown mode, where users write Markdown syntax directly.

The markdown toolbar button switches between them. Switching does not move or convert content. Whatever you left in Rich Text mode is still there when you come back, and the same is true of Markdown mode. Each mode owns its content for the whole life of the editor session.

Why the two are kept separate

This is a deliberate design decision, and it exists to protect content.

HTML expresses far more than Markdown can. Rich text content routinely holds tables with merged cells, styled spans, custom classes, embedded media, and inline attributes that have no Markdown equivalent. If the toolbar button converted automatically on every switch, each toggle would quietly discard whatever could not be represented in the target format. Flip to Markdown and back, and you would not return to where you started. A user checking the Markdown view out of curiosity would pay for it with degraded formatting.

Keeping the two separate means toggling is always safe. A user can move between modes as often as they like, and nothing is lost in either direction. The tradeoff is that content does not travel between modes on its own, which is why getMarkdown() and setMarkdown() exist as explicit, deliberate operations on the Markdown side.

Inside each mode, the layout differs.

Rich Text mode has two views you can flip between with the Code View toolbar button. The Rich Text view shows formatted content. The Code View shows the underlying HTML. These two views do convert content between them: edit formatted text, switch to Code View, and you see the HTML for it. Edit that HTML, switch back, and you see it rendered.

Markdown mode has a single view split into two panes. The Markdown pane is where you write. The result pane beside it shows a live preview of what your Markdown renders to. The preview pane is view-only, so users edit Markdown in one pane and watch the result in the other.

Here is the whole picture:

Where you are What it shows Editable
Rich Text view Formatted content Yes
Code View HTML for that content Yes
Markdown pane Markdown syntax Yes
Preview pane Rendered result No

In practice this means a document belongs to one mode. Since nothing carries content across, you decide up front whether a given piece of content is a Markdown document or a rich text document, and your users work in that mode.

Set Up the Editor

Everything below builds on this initialization.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Froala Markdown Demo</title>

  <!-- Core Froala Editor styles -->
  <link
    href="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/css/froala_editor.pkgd.min.css"
    rel="stylesheet"
  />
  <!-- Markdown plugin styles -->
  <link
    href="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/css/plugins/markdown.min.css"
    rel="stylesheet"
  />
</head>
<body>
  <div id="editor"></div>
  <button id="download-md">Download .md</button>

  <!-- Core Froala Editor script -->
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/js/froala_editor.pkgd.min.js"></script>
  <!-- Markdown plugin script -->
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/js/plugins/markdown.min.js"></script>

  <script>
    const editor = new FroalaEditor('#editor', {
      // Required. Without this, editor.markdown is undefined.
      pluginsEnabled: ['markdown'],
      // The 'markdown' button switches the user between the two modes.
      toolbarButtons: ['bold', 'italic', 'underline', '|', 'markdown'],
      events: {
        'markdown.beforeSet': function (markdown) {
          // Return false here to reject the write entirely.
          if (!markdown || !markdown.trim()) {
            console.warn('Refusing to load empty Markdown.');
            return false;
          }
        },
        'markdown.set': function () {
          console.log('Markdown content applied.');
        },
        'markdown.afterGet': function (markdown) {
          console.log('Markdown read, length:', markdown.length);
        }
      }
    });
  </script>
</body>
</html>

Load Markdown Content Into the Editor

This is the inbound half of the workflow. Content arrives as a Markdown string from a CMS field, a GitHub README, or a generated draft, and you need it inside the editor.

setMarkdown() takes that string and writes it to the Markdown side of the editor, replacing whatever was there.

async function loadDraft(editor, draftId) {
  const response = await fetch(`/api/drafts/${draftId}`);
  const draft = await response.json();

  const result = editor.markdown.setMarkdown(draft.markdownBody);

  if (result === false) {
    // Either the value wasn't a string, or markdown.beforeSet cancelled it.
    console.error('Draft content was rejected.');
    return;
  }

  // result is true if Markdown mode was on and the content is now visible.
  // result is null if Markdown mode was off and the content was cached.
  console.log('Draft loaded. Applied immediately:', result === true);
}

Understanding the three return values

setMarkdown() gives you one of three answers, and each one tells you something specific:

  • true means Markdown mode was enabled. The content was applied immediately and an undo step was saved, so the user can reverse it.
  • null means Markdown mode was disabled. The value was cached and will be applied the next time Markdown mode is turned on. It can also be read back through getMarkdown() in the meantime. Nothing visible happens at call time.
  • false means the value you passed was not a string, or a markdown.beforeSet listener returned false and cancelled the write.

The null case is worth dwelling on, because it is doing more work than it first appears.

When you call setMarkdown() from Rich Text mode, the editor has a choice to make. It could refuse the write. It could force the user into Markdown mode so the content has somewhere visible to land. Froala does neither. It accepts the content, holds it, applies it the moment Markdown mode next opens, and hands you a distinct return value saying so. The user is not yanked out of the view they were working in, and your content is not dropped.

That is why the return type is true, null, or false rather than a plain boolean. Three states tell you three different things: applied and visible, accepted and waiting, or rejected. You can branch on all three.

Treat null as success. The only failure signal is false.

Repopulate on every initialization

Markdown content does not survive across page loads or editor re-initialization. Every fresh editor starts with the Markdown side empty until something calls setMarkdown().

That makes this call part of your setup path, not just something behind a “Load draft” button:

const editor = new FroalaEditor('#editor', {
  pluginsEnabled: ['markdown'],
  toolbarButtons: ['bold', 'italic', 'underline', '|', 'markdown'],
  events: {
    initialized: function () {
      // Restore stored Markdown as soon as the editor is ready.
      const stored = window.localStorage.getItem('draft-markdown');
      if (stored) {
        this.markdown.setMarkdown(stored);
      }
    }
  }
});

If you skip this, users who refresh the page find their Markdown gone, which reads as data loss even though nothing was ever persisted in the first place.

Save Markdown Content as a .md File

Now the outbound half. getMarkdown() returns the Markdown content as a string.

Be clear about what it reads. It returns the Markdown side of the editor, which means it gives you real content when the document was authored in Markdown mode or populated through setMarkdown(). It does not convert a user’s rich text into Markdown. If someone typed formatted content in Rich Text mode and you call getMarkdown(), you get whatever is on the Markdown side, which may well be empty.

The method also behaves differently depending on the current mode. With Markdown mode enabled you get the live content. With Markdown mode disabled you get the last cached value, and the editor’s HTML is never consulted.

function downloadMarkdown(editor, filename = 'content.md') {
  const markdown = editor.markdown.getMarkdown();

  // A markdown.beforeGet listener can cancel the read.
  if (markdown === false) {
    console.warn('Markdown read was cancelled.');
    return;
  }

  const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();

  URL.revokeObjectURL(url);
}

document.getElementById('download-md').addEventListener('click', function () {
  downloadMarkdown(editor, 'article.md');
});

Always check for false before writing to a file. A cancelled read returns the boolean, and passing that straight into a Blob produces a file containing the text “false”.

This is what makes docs-as-code workflows practical. The .md file that comes out is diffable in Git, reviewable in a pull request, and consumable by any static site generator, without the HTML noise that makes content diffs unreadable.

The markdown.afterGet event fires on every successful read and receives the string, which makes it a convenient hook for logging exports or triggering analytics.

Replace the Custom Export to Markdown Button

If you followed our earlier guide to adding a custom Export from Markdown button, you built a toolbar button that worked around the absence of a getter. Retrieving the content took four calls:

// The older pattern, before getMarkdown() existed.
this.selection.save();
this.commands.selectAll();
const markdownContent = this.selection.text();
this.selection.restore();

That button also needed a refresh handler checking markdown.isEnabled(), so it could disable itself outside Markdown mode, plus an SVG entry, a DefineIcon call, and a RegisterCommand block.

All of it collapses into one line:

const markdown = editor.markdown.getMarkdown();

Both approaches require Markdown mode, so this is a like-for-like swap rather than a new capability. What you gain is less code to own, a cursor that is never touched, and the markdown.beforeGet and markdown.afterGet events, which the selection-based approach had no way to offer. The download half of the old callback still applies unchanged, and it is the same Blob code shown earlier.

Common Pitfalls

Know which mode you are in before you save. The getters follow the active mode, so html.get() returns Markdown text while Markdown mode is on. That consistency is useful once you expect it, since a single save handler can serve both modes as long as it records which format it received. Branch on the current mode and store the format alongside the content:

function getContentForStorage(editor) {
  return editor.markdown.isEnabled()
    ? { format: 'markdown', body: editor.markdown.getMarkdown() }
    : { format: 'html', body: editor.html.get() };
}

setMarkdown() replaces the Markdown content rather than appending to it. That is the documented behavior and it matches html.set(), but it applies to cached writes too. If you call it while Markdown mode is off, the queued value will replace whatever the user had written there once they switch back. Prompt for confirmation when you are loading over content a user may have authored.

Conclusion

The single thing to remember is that Markdown content and rich text content live separately in Froala, so a user can move between modes without losing anything in either one. Every part of the API follows from that: getMarkdown() and setMarkdown() operate on the Markdown side deliberately, null tells you a write is waiting for a mode that is not open yet, and the getters report the mode you are actually in. Build around it and Markdown becomes a first-class storage format rather than a one-way export. For the complete method and event reference, see the Froala Markdown plugin documentation.

FAQ

Why does setMarkdown() return null?

Markdown mode was off when you called it, so rather than dropping your content or forcing the user into a different view, the editor cached the value. It applies the next time Markdown mode turns on, and you can read it back with getMarkdown() in the meantime. The write succeeded. Only false indicates a rejected call.

Does switching between Markdown mode and Rich Text mode convert my content?

No, and that is intentional. HTML can express formatting that Markdown cannot, so converting on every toggle would discard a little more of your content each time. Keeping the two separate means switching is always safe. You may be thinking of Code View, which is a different feature and does convert between formatted content and HTML within Rich Text mode.

Can I load a Markdown string and let users edit it as formatted rich text?

Convert it to HTML in your application first, with a library such as marked.js, then call html.set(). setMarkdown() writes to the Markdown side of the editor, which is where users edit Markdown syntax directly.

Does Markdown content persist after a page refresh?

No. Every editor initialization starts with the Markdown side empty. Store the string yourself and call setMarkdown() when the editor initializes.
graphical user interface, text

Froala v5.4.0: AI Chat Assistant, Reviewable AI Edits, and Markdown

Froala v5.4.0

Froala v5.4.0 is our biggest AI release yet. You get a full AI Chat Assistant that lives in a side panel next to your content, AI Improve Writing that shows every suggested edit as an accept or reject change instead of overwriting your text, and Markdown getter and setter methods so Markdown can finally be the format you store, not just something you convert once. Collaboration also gets a solid upgrade, with track changes now covering drag and drop moves and code snippet edits.

AI Chat Assistant

 

Top Highlights

1. AI Chat Assistant: a conversation next to your document

What it is

A new AI Chat icon in the toolbar opens a collapsible panel docked to the right of the editor. Instead of firing a single prompt and dropping the answer into your content, you can now have a back and forth conversation: ask a question, get an answer, ask a follow-up and only then decide whether anything goes into the document.

You choose what the AI can see, using one or more context sources at the same time:

  • Current Document: sends the whole editor content.
  • Selected Content: sends only the text you highlighted.
  • Uploaded files: PDF, DOCX, TXT, Markdown, and images (JPG, PNG, GIF, WebP).
  • Reference URL: a link the AI can pull context from.

The panel also includes a web search toggle, a model dropdown, and a reasoning toggle so users can pick a fast model for quick questions or a stronger one for heavier work. The chat is provider agnostic, so it works with whichever AI service you already have configured.

When the AI proposes a rewrite, you review it before it lands. In normal mode, clicking Apply to document opens the same review popup used elsewhere in the editor, with additions and removals highlighted, and the affected part of the document is visibly highlighted the moment you click Apply. In collaboration mode, the suggestion appears as a Suggested rewrite card right inside the chat panel with a preview toggle and an Insert button.

Why it matters

Writers stop tab-switching to a separate AI tool and pasting content back and forth. Reviewers get a safety net, because nothing changes in the document until a human approves it. And because the model list is supplied by you rather than hard-coded by us, you stay in control of cost, provider, and data handling.

How to use it

Add the AI Chat button to your toolbar and supply the models you want users to choose from:

new FroalaEditor('#editor', {
  aiSupplementalTermsAccepted: true,
  toolbarButtons: ['bold', 'italic', 'aiChatAssistant'],
  aiChatModels: [
    { id: 'fast-model', name: 'Fast', webSearch: false, reasoning: false },
    { id: 'smart-model', name: 'Smart', webSearch: true, reasoning: true }
  ],
  aiChatDefaultModel: 'fast-model',
  aiChatWelcomeMessage: 'Hi! Ask me anything about this document.',
  aiAssistRequest: function (data, signal) {
    return fetch('https://your-ai-backend/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: signal
    })
      .then(response => response.json())
      .then(result => result.answer);
  }
});

If you do not configure any models, the model dropdown simply does not appear.

Agnostic AI rich text editor

Good to know

  • Only one right-docked panel is open at a time. Opening AI Chat closes the Collaboration comments panel, and the other way around.
  • The panel can be resized, but the width is not stored by the editor. Listen for the resize and pass the width back in yourself if you want it remembered.
  • When the editor is disabled or read only, the chat panel dims and stops responding too.
  • The Copy button on a chat message now copies formatted content, so bold text, lists, and headings survive when you paste into an email or another document.

2. AI Improve Writing with accept and reject changes

What it is

Select text in the editor and a floating action button appears. Click Improve Writing and only that selection is sent to your AI provider. The result comes back as tracked changes: removed text is struck through, added text is highlighted, and you can step through the changes one at a time and accept or reject each one, or use Accept All and Reject All. The original content stays untouched until you accept.

The behaviour adapts to how your editor is set up:

Setup Where the diff appears What happens on accept
Collaboration off Inline in the editor The change is applied in place
Collaboration on, Editing mode In a review popup The change is inserted as a normal edit and syncs to everyone
Collaboration on, Suggesting mode In a review popup The change becomes a tracked suggestion for others to review

Why it matters

AI output is a proposal, not a decision. Staging the diff means an author never has to undo a bad rewrite, and in a shared document it means half-finished AI suggestions never appear in a teammate’s screen before anyone approved them.

How to use it

new FroalaEditor('#editor', {
  aiSupplementalTermsAccepted: true,
  aiImproveWritingPrompt: 'Improve the clarity and grammar of this text.',
  aiAssistRequest: function (data, signal) {
    return fetch('https://your-ai-backend/improve', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: data.prompt }),
      signal: signal
    })
      .then(response => response.json())
      .then(result => result.answer);
  }
});

Under the hood, the comparison logic behind this feature is now a single shared module used by Improve Writing, the AI Chat previews, and the collaborative version control Compare With view.

AI improve writing

3. Markdown in, Markdown out

What it is

Two new methods, getMarkdown() and setMarkdown(), work exactly like html.get() and html.set() do for HTML. Authors edit in the familiar rich text interface, but your application reads and writes Markdown. Four lifecycle events let you hook into the flow: markdown.beforeGet, markdown.afterGet, markdown.beforeSet, and markdown.set.

Why it matters

If your product stores content in Markdown, the way GitHub comments or Asana descriptions do, you no longer have to choose between a good editing experience and keeping Markdown as your source of truth. Content round-trips cleanly: load Markdown, edit visually, save Markdown, load it again later and keep editing.

How to use it

const editor = new FroalaEditor('#editor', {
  pluginsEnabled: ['markdown'],
  events: {
    'markdown.afterGet': function (markdown) {
      console.log('Markdown fetched:', markdown);
    },
    'markdown.beforeSet': function (markdown) {
      // Return false to cancel setMarkdown()
      if (!markdown.trim()) {
        return false;
      }
    }
  }
});

// Load stored Markdown into the editor
editor.setMarkdown('# Release notes\n\nWritten in Markdown.');

// Read it back out to save
const content = editor.getMarkdown();

getMarkdown() reads live editor content and converts it when Markdown mode is on, or returns the cached value when it is off. setMarkdown() does the reverse. We also fixed a long standing annoyance where a Shift+Enter line break came back as a literal <br> in your Markdown text. It now returns as a proper line break, and it stays clean no matter how many times you edit the content.

4. Track changes now covers moves and code snippets

What it is

Two gaps in collaborative Suggesting mode are closed.

Drag and drop is now tracked. Dragging an image, table, video, or a block of selected text creates one linked pair of suggestions: a red struck-through marker where the content used to be, and a green highlighted copy where it now sits. The suggestions panel shows this as a single Moved card rather than two unrelated Added and Removed cards, and accepting or rejecting either half automatically resolves the other. Dragging an already-pending move a second time is recognized as the same proposal, so you never build up a pile of stray cards.

Code snippet edits and deletes are now tracked. Previously, inserting a code snippet created a suggestion but editing or deleting one applied silently. Editing is now recorded as a single in-place Replace suggestion, and deleting wraps the snippet as a normal delete suggestion.

code snippets track changes

Why it matters

Reviewers can trust the suggestions panel again. If a change happened in the document, it shows up as something a reviewer can accept or reject, with no silent edits slipping through and no confusing duplicate cards.

Rich text editor with track changes

Also in v5.4.0

A few smaller AI and collaboration additions worth knowing about:

  • Speech to text in AI Assist. A microphone button lets users dictate a prompt instead of typing it. Audio is captured in the browser and transcribed through your existing aiAssistRequest handler, so it works in Chrome, Firefox, and Safari with no extra service. Controlled by aiSpeechToText (default true), plus aiSpeechToTextWaveform, aiSpeechToTextListeningText, and aiSpeechToTextMaxDuration (default 120 seconds).
    Speech to text in AI Assist
  • Inline ghost text autocomplete. An opt-in, Smart Compose style suggestion that appears in grey at your cursor when you pause at the end of a block. Press Tab to accept, Esc to dismiss, or just keep typing. Configured with aiAssistAutoComplete, aiAssistAutoCompleteDelay, and aiAssistAutoCompletePromptTemplate. Ghost text is stripped before html.get(), so it can never leak into saved content.
    Inline ghost text autocomplete
  • AI Shortcuts menu cleanup. The Tone and Translate menus are now hidden when no options are configured, and inserting AI content causes far less layout shift.
  • Collaboration fixes. The user name label now appears on the very first character typed, suggestion cards get a proper reply field with Cancel and Reply buttons, and word replacements made through the browser spell check menu are tracked correctly.

A full list of the remaining enhancements and bug fixes is on the v5.4.0 changelog page.

Upgrading to v5.4.0

1. Update the package

# npm
npm install froala-editor@5.4.0

# yarn
yarn upgrade froala-editor@5.4.0

# pnpm
pnpm update froala-editor@5.4.0

If you use a framework wrapper, update it alongside the core package, for example npm install react-froala-wysiwyg@5.4.0.

2. Or update your CDN links

<link href="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/css/froala_editor.pkgd.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/js/froala_editor.pkgd.min.js"></script>

3. Enable what you want to use. The AI Chat panel needs the aiChatAssistant toolbar button and an aiAssistRequest handler. Inline autocomplete is off by default and needs aiAssistAutoComplete: true. Markdown getter and setter methods need the markdown plugin enabled.

5. Clear your build cache and rebuild, then test any custom AI or collaboration integration against the new shared review flow.

Coming from Froala 3 or 4? Start with the migration guide, then apply the steps above.

Try it out

The fastest way to get a feel for v5.4.0 is to use it.

We would love to hear what you build with it. If something does not behave the way you expect, open an issue and tell us.

How to Autosave Froala Content and Recover Drafts in React

Froala React autosave

Losing an hour of writing because a tab crashed is one of the fastest ways to lose a user’s trust in your app. If your React app uses Froala Editor for rich text, you can fix this with a lightweight autosave system that writes drafts to localStorage instantly and syncs to your server every few seconds.

This guide walks you through building that system from scratch, including a “Saving / Saved” status indicator, a “Restore draft” banner on reload, and a warning that stops users from accidentally closing the tab with unsaved work.

Key Takeaways

  • Use the contentChanged event, not keyup, to trigger saves. It fires once per meaningful edit instead of on every keystroke.
  • Debounce your save logic with a useRef timer so you don’t flood localStorage or your API during fast typing.
  • Save to localStorage on every debounced change (cheap and instant), but only hit your API every few seconds (expensive and rate-limited).
  • Leave immediateReactModelUpdate off unless another part of your UI needs to read the editor’s content on every keystroke.
  • Add a beforeunload listener to warn users before they lose unsaved changes.
  • This custom setup is different from Froala’s built-in autosave plugin (saveInterval, saveURL). Use the built-in plugin for simple, timer-based server saves. Use this guide’s approach when you also want local draft recovery and change-based saving.

Prerequisites

Before you start, make sure you have:

  • Froala Editor installed (froala-editor and react-froala-wysiwyg)
  • React 18 or later
  • A basic backend endpoint (or a mock one) that can accept a POST request with draft content
  • Familiarity with React hooks (useState, useEffect, useRef)

Install the packages if you haven’t already:

npm install froala-editor react-froala-wysiwyg

Why Autosave Matters for Rich Text Editors

Rich text content is expensive to lose. Unlike a single form field, a Froala document might represent 20 minutes of formatting, image placement, and careful wording. Users don’t expect to lose that to a refresh, a closed laptop lid, or a flaky network connection.

An autosave system solves three problems at once:

  1. Local safety net. localStorage writes are synchronous and don’t depend on the network, so you get instant protection against tab crashes and accidental refreshes.
  2. Server durability. localStorage is scoped to one browser. If the user switches devices, they need their draft saved to your backend too.
  3. User confidence. A visible “Saving…” or “Saved” indicator tells users their work is safe, so they stop hitting Ctrl+S out of habit.

This Guide Goes Beyond Froala’s Built-in Autosave Plugin

Froala ships its own autosave plugin with options like saveInterval, saveURL, and saveMethod. It’s worth knowing what that plugin covers before you write any custom code, so you can decide which approach fits your app.

The built-in plugin fires an HTTP request to your server on a fixed timer. This guide builds something different, for three reasons:

  • A local safety net. The built-in plugin only talks to your server. If the request fails or the tab closes mid-cycle, there’s no local backup. This guide adds a localStorage layer that saves instantly and works offline.
  • A restore experience. The built-in plugin has no concept of “here’s a draft you can recover.” This guide adds a banner that lets users choose between a local draft and their last saved server version.
  • Change-based saving instead of a fixed clock. The built-in plugin saves on a timer regardless of whether anything changed. This guide debounces saves based on actual edits, so you get faster local protection and fewer wasted server requests.

If you only need periodic server saves with no local fallback or recovery UI, the built-in plugin is fewer lines of code and worth using instead. The rest of this guide is for cases where you want the fuller setup: instant local saves, controlled server syncs, and a way for users to recover unsaved work.

Step 1. Set Up the Froala Editor Component

Start with a standard Froala setup in a React functional component. Keep the editor’s config minimal at first, you’ll add autosave logic on top of it.

import React, { useState, useRef, useEffect, useCallback } from 'react';
import FroalaEditor from 'react-froala-wysiwyg';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/css/froala_editor.pkgd.min.css';

const DRAFT_KEY = 'froala-draft-content';
const SAVE_INTERVAL_MS = 5000; // sync to API every 5 seconds
const DEBOUNCE_MS = 800; // wait this long after typing stops

function AutosaveEditor() {
  const [content, setContent] = useState('');
  const [saveStatus, setSaveStatus] = useState('idle'); // idle | saving | saved | error
  const [showRestoreBanner, setShowRestoreBanner] = useState(false);

  return (
    <div className="editor-wrapper">
      <FroalaEditor
        model={content}
        onModelChange={setContent}
        config={{ placeholderText: 'Start writing your draft...' }}
      />
    </div>
  );
}

export default AutosaveEditor;

This gives you a working editor with two-way binding. Now you’ll layer autosave on top without touching this core setup.

Step 2. Use contentChanged, Not keyup

It’s tempting to hook into keyup because it feels like the most direct signal that “something happened.” Don’t. keyup fires on every single keystroke, including arrow keys, backspace, and shortcut combinations that don’t even change the content. That’s a lot of noise for something that’s supposed to trigger a save.

contentChanged is Froala’s dedicated event for actual content mutations. It fires after the internal editor model updates, so you know the HTML genuinely changed. This is also the event Froala’s own React integration uses internally to keep the React model in sync, so you’re working with the grain of the library rather than against it.

Here’s how you register it through the events config:

const editorConfig = {
  placeholderText: 'Start writing your draft...',
  events: {
    contentChanged: function () {
      // 'this' refers to the Froala editor instance here
      handleContentChanged(this.html.get());
    },
  },
};

Why this matters: if you based autosave on keyup, a user who pastes an image or uses the toolbar to bold a selection (no keyboard involved) wouldn’t trigger a save at all. contentChanged catches all of these cases because it listens for actual document mutations, not input device events.

Step 3. Debounce Saves With a useRef Timer

If you save on every contentChanged event without debouncing, you’ll write to localStorage and call your API dozens of times a minute during active typing. That’s wasteful and, for the API call, can trip rate limits or create a backlog of requests.

The fix is a debounce pattern using useRef to hold the timer ID. A ref is the right tool here because you need a mutable value that persists across renders without triggering a re-render itself, which is exactly what a timer ID needs.

function AutosaveEditor() {
  const [content, setContent] = useState('');
  const [saveStatus, setSaveStatus] = useState('idle');
  const [showRestoreBanner, setShowRestoreBanner] = useState(false);

  // Holds the debounce timer so it survives across re-renders
  const debounceTimerRef = useRef(null);
  // Holds the interval timer for periodic API syncs
  const apiSyncIntervalRef = useRef(null);
  // Tracks whether there's unsaved content since the last API sync
  const hasUnsavedChangesRef = useRef(false);

  const saveToLocalStorage = useCallback((html) => {
    try {
      localStorage.setItem(DRAFT_KEY, html);
    } catch (err) {
      // localStorage can throw if storage is full or disabled
      console.error('Failed to save draft locally:', err);
    }
  }, []);

  const saveToApi = useCallback(async (html) => {
    setSaveStatus('saving');
    try {
      await fetch('/api/drafts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: html }),
      });
      setSaveStatus('saved');
      hasUnsavedChangesRef.current = false;
    } catch (err) {
      console.error('Failed to save draft to API:', err);
      setSaveStatus('error');
    }
  }, []);

  const handleContentChanged = useCallback((html) => {
    // Clear any pending debounce so rapid typing doesn't stack up saves
    if (debounceTimerRef.current) {
      clearTimeout(debounceTimerRef.current);
    }

    debounceTimerRef.current = setTimeout(() => {
      // localStorage writes are cheap, so this happens on every debounced change
      saveToLocalStorage(html);
      hasUnsavedChangesRef.current = true;
    }, DEBOUNCE_MS);
  }, [saveToLocalStorage]);

  // ...editor config and JSX go below
}

Notice the two different debounce strategies happening here. The localStorage write happens on a short debounce (800ms) because it’s fast and local. The API sync happens on a separate, longer interval, which you’ll wire up next.

Step 4. Sync to Your API on an Interval, Not on Every Change

Debouncing alone isn’t quite enough for the API call. Even with an 800ms debounce, a user who types continuously for two minutes would still fire a save every time they pause, which can still add up to more requests than your backend needs to handle.

Instead, use a separate setInterval that checks a flag and only sends a request if there’s actually unsaved content:

useEffect(() => {
  apiSyncIntervalRef.current = setInterval(() => {
    if (hasUnsavedChangesRef.current) {
      saveToApi(content);
    }
  }, SAVE_INTERVAL_MS);

  return () => {
    clearInterval(apiSyncIntervalRef.current);
  };
}, [content, saveToApi]);

This gives you a clean separation of responsibilities. localStorage acts as your instant, per-change safety net. The API call acts as your durable, every-few-seconds sync. Neither one blocks the other, and the user’s typing never stalls waiting on a network request.

Step 5. Add a Saving and Saved Status Indicator

A status indicator closes the loop for the user. Without it, autosave is invisible, and invisible safety nets don’t build trust. Keep this simple with a small conditional render based on the saveStatus state you’re already tracking.

function SaveIndicator({ status }) {
  const statusConfig = {
    idle: { text: '', color: '#999' },
    saving: { text: 'Saving...', color: '#f0ad4e' },
    saved: { text: 'Saved', color: '#5cb85c' },
    error: { text: 'Save failed, retrying...', color: '#d9534f' },
  };

  const current = statusConfig[status] || statusConfig.idle;

  if (!current.text) return null;

  return (
    <span style={{ color: current.color, fontSize: '13px', marginLeft: '8px' }}>
      {current.text}
    </span>
  );
}

Drop it right next to your editor’s toolbar area:

<div className="editor-header">
  <h3>Draft</h3>
  <SaveIndicator status={saveStatus} />
</div>

This small piece of UI does a lot of work. Users glance at it, see “Saved,” and move on with confidence.

Step 6. Offer a Restore Draft Banner on Reload

Now handle the recovery side. When the component mounts, check localStorage for an existing draft and, if one exists and differs from what the server has, show a banner offering to restore it.

useEffect(() => {
  const savedDraft = localStorage.getItem(DRAFT_KEY);
  if (savedDraft && savedDraft.trim().length > 0) {
    setShowRestoreBanner(true);
  }
}, []);

const handleRestoreDraft = () => {
  const savedDraft = localStorage.getItem(DRAFT_KEY);
  if (savedDraft) {
    setContent(savedDraft);
  }
  setShowRestoreBanner(false);
};

const handleDiscardDraft = () => {
  localStorage.removeItem(DRAFT_KEY);
  setShowRestoreBanner(false);
};

And the banner itself:

{showRestoreBanner && (
  <div className="restore-banner">
    <span>We found an unsaved draft from your last session.</span>
    <button onClick={handleRestoreDraft}>Restore draft</button>
    <button onClick={handleDiscardDraft}>Discard</button>
  </div>
)}

This pattern gives users control instead of silently overwriting whatever they last saved to the server. Some users will want their last-known-good server copy, others will want the more recent local draft. Let them choose.

Step 7. Warn Before Unload With beforeunload

Even with autosave running, there’s a small window between the last successful save and the current unsaved keystrokes. Cover that gap by warning the user if they try to close the tab while hasUnsavedChangesRef.current is true.

useEffect(() => {
  const handleBeforeUnload = (event) => {
    if (hasUnsavedChangesRef.current) {
      event.preventDefault();
      // Most modern browsers show a generic message regardless of this string
      event.returnValue = '';
    }
  };

  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
  };
}, []);

Modern browsers ignore custom text in this dialog for security reasons, but calling event.preventDefault() and setting event.returnValue is still what triggers the browser’s built-in warning dialog. This is a small addition that catches the rare case where a user closes the tab in the few seconds before your next scheduled API sync.

When to Use immediateReactModelUpdate

Froala’s React wrapper ships an option called immediateReactModelUpdate. When set to true, it updates your React model on every keyup event instead of waiting for contentChanged.

When it helps: if another part of your UI needs to reflect the editor’s content in real time, like a live word counter that updates mid-keystroke, or a preview pane that mirrors the editor exactly as the user types, this option keeps your React state perfectly in sync with the DOM.

When it costs you: for autosave specifically, you don’t need this. Autosave only cares about the settled state of the content after a pause, not every intermediate keystroke. Turning this on means React re-renders on every key release, which adds unnecessary overhead, especially in documents with a lot of content or complex formatting. Leave it at its default of false and let contentChanged plus your debounce handle everything.

const editorConfig = {
  immediateReactModelUpdate: false, // Keep this off for autosave-only use cases
  events: {
    contentChanged: function () {
      handleContentChanged(this.html.get());
    },
  },
};

Full Code For The React Component

import React, { useState, useRef, useEffect, useCallback } from 'react';
import FroalaEditor from 'react-froala-wysiwyg';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/css/froala_editor.pkgd.min.css';

const DRAFT_KEY = 'froala-draft-content';
const SAVE_INTERVAL_MS = 30000; // sync to API every 30 seconds
const DEBOUNCE_MS = 800; // wait this long after typing stops before saving locally

function SaveIndicator({ status }) {
  const statusConfig = {
    idle: { text: '', color: '#999' },
    saving: { text: 'Saving...', color: '#f0ad4e' },
    saved: { text: 'Saved', color: '#5cb85c' },
    error: { text: 'Save failed, retrying...', color: '#d9534f' },
  };

  const current = statusConfig[status] || statusConfig.idle;

  if (!current.text) return null;

  return (
    <span style={{ color: current.color, fontSize: '13px', marginLeft: '8px' }}>
      {current.text}
    </span>
  );
}

function AutosaveEditor() {
  const [content, setContent] = useState('');
  const [saveStatus, setSaveStatus] = useState('idle'); // idle | saving | saved | error
  const [showRestoreBanner, setShowRestoreBanner] = useState(false);

  // Holds the debounce timer so it survives across re-renders without causing them
  const debounceTimerRef = useRef(null);
  // Holds the interval timer for periodic API syncs
  const apiSyncIntervalRef = useRef(null);
  // Tracks whether there's unsaved content since the last successful API sync
  const hasUnsavedChangesRef = useRef(false);
  // Mirrors the latest content so the interval callback always sees the current value
  const latestContentRef = useRef('');

  // --- Save to localStorage (fast, local, synchronous) ---
  const saveToLocalStorage = useCallback((html) => {
    try {
      localStorage.setItem(DRAFT_KEY, html);
    } catch (err) {
      // localStorage can throw if storage is full or disabled (e.g. private browsing)
      console.error('Failed to save draft locally:', err);
    }
  }, []);

  // --- Save to your backend API ---
  const saveToApi = useCallback(async (html) => {
    setSaveStatus('saving');
    try {
      await fetch('/api/drafts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: html }),
      });
      setSaveStatus('saved');
      hasUnsavedChangesRef.current = false;
    } catch (err) {
      console.error('Failed to save draft to API:', err);
      setSaveStatus('error');
    }
  }, []);

  // --- Debounced handler wired to the editor's contentChanged event ---
  const handleContentChanged = useCallback(
    (html) => {
      latestContentRef.current = html;

      // Clear any pending debounce so rapid typing doesn't stack up saves
      if (debounceTimerRef.current) {
        clearTimeout(debounceTimerRef.current);
      }

      debounceTimerRef.current = setTimeout(() => {
        saveToLocalStorage(html);
        hasUnsavedChangesRef.current = true;
      }, DEBOUNCE_MS);
    },
    [saveToLocalStorage]
  );

  // --- Periodic API sync, only fires a request if something actually changed ---
  useEffect(() => {
    apiSyncIntervalRef.current = setInterval(() => {
      if (hasUnsavedChangesRef.current) {
        saveToApi(latestContentRef.current);
      }
    }, SAVE_INTERVAL_MS);

    return () => {
      clearInterval(apiSyncIntervalRef.current);
    };
  }, [saveToApi]);

  // --- Clean up the debounce timer on unmount to avoid a stray setTimeout ---
  useEffect(() => {
    return () => {
      if (debounceTimerRef.current) {
        clearTimeout(debounceTimerRef.current);
      }
    };
  }, []);

  // --- Check for a recoverable draft on mount ---
  useEffect(() => {
    const savedDraft = localStorage.getItem(DRAFT_KEY);
    if (savedDraft && savedDraft.trim().length > 0) {
      setShowRestoreBanner(true);
    }
  }, []);

  const handleRestoreDraft = () => {
    const savedDraft = localStorage.getItem(DRAFT_KEY);
    if (savedDraft) {
      setContent(savedDraft);
      latestContentRef.current = savedDraft;
    }
    setShowRestoreBanner(false);
  };

  const handleDiscardDraft = () => {
    localStorage.removeItem(DRAFT_KEY);
    setShowRestoreBanner(false);
  };

  // --- Warn before the tab closes if there's unsaved work ---
  useEffect(() => {
    const handleBeforeUnload = (event) => {
      if (hasUnsavedChangesRef.current) {
        event.preventDefault();
        // Most modern browsers show a generic message regardless of this string
        event.returnValue = '';
      }
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, []);

  const editorConfig = {
    placeholderText: 'Start writing your draft...',
    // Keep this off for autosave-only use cases. Turning it on updates the React
    // model on every keyup, which costs a re-render per keystroke you don't need
    // here since contentChanged plus the debounce already covers autosave timing.
    immediateReactModelUpdate: false,
    events: {
      contentChanged: function () {
        // 'this' refers to the Froala editor instance here
        handleContentChanged(this.html.get());
      },
    },
  };

  return (
    <div className="editor-wrapper">
      {showRestoreBanner && (
        <div className="restore-banner">
          <span>We found an unsaved draft from your last session.</span>
          <button onClick={handleRestoreDraft}>Restore draft</button>
          <button onClick={handleDiscardDraft}>Discard</button>
        </div>
      )}

      <div className="editor-header">
        <h3>Draft</h3>
        <SaveIndicator status={saveStatus} />
      </div>

      <FroalaEditor
        model={content}
        onModelChange={setContent}
        config={editorConfig}
      />
    </div>
  );
}

export default AutosaveEditor;

How to Test React Autosave and Draft Recovery

Autosave logic is easy to get subtly wrong, since most of it happens in the background with no visible feedback beyond the status indicator. Work through these checks manually before you ship it.

1. Confirm contentChanged is firing, and keyup isn’t needed. Open your browser’s DevTools console and add a temporary log inside handleContentChanged. Type a few words, then use the toolbar to bold a selection or paste an image with no keyboard involved. Both should log. If the toolbar or paste actions don’t trigger a save, you’ve likely wired the listener to keyup instead of contentChanged.

const handleContentChanged = useCallback((html) => {
  console.log('contentChanged fired', html.length, 'chars'); // remove before shipping
  // ...debounce logic
}, []);

2. Verify the debounce is actually debouncing. Type continuously for five seconds without pausing. Check localStorage in DevTools (Application tab -> Local Storage) while typing. The froala-draft-content key should not update on every keystroke, it should only update roughly 800ms after you stop typing. If you see it updating constantly, the clearTimeout call is likely missing or not clearing the right ref.

3. Confirm the API syncs on interval, not on every change. Open the Network tab, filter for your /api/drafts endpoint, and type continuously for 20-30 seconds. You should see requests roughly every 5 seconds, not one per keystroke and not one per debounced local save. If requests fire more often than SAVE_INTERVAL_MS, check that hasUnsavedChangesRef is being read inside the interval callback rather than recreating the interval on every render.

4. Test the “no unnecessary requests” behavior. Let the editor sit idle for 10+ seconds after your last edit and watch the Network tab. No new requests should appear, since hasUnsavedChangesRef.current should be false after a successful save. If requests keep firing on an empty editor, the flag isn’t being reset in saveToApi.

5. Test draft recovery end to end. Type some content, wait for the local save to fire (check localStorage for the updated value), then refresh the page without submitting or navigating away cleanly. The restore banner should appear. Click “Restore draft” and confirm the editor repopulates with your content. Then repeat the test and click “Discard” instead, confirming the banner disappears and the localStorage key is cleared.

6. Test the offline / failed-save path. Open DevTools, go to the Network tab, and set throttling to “Offline.” Type some content and wait past your SAVE_INTERVAL_MS. The indicator should show your error state. Go back online and wait for the next interval tick, the indicator should flip to “Saved” without you needing to type anything else, since hasUnsavedChangesRef stays true until a save actually succeeds.

7. Test the beforeunload warning. Type content, don’t wait for it to save, and try closing the tab or navigating away immediately. Your browser should show its built-in “leave site” confirmation dialog. Then let content fully save (indicator shows “Saved”), and try closing again, this time there should be no warning.

8. Sanity-check immediateReactModelUpdate performance if you ever turn it on. If you’re experimenting with immediateReactModelUpdate: true for another feature, open the React DevTools Profiler, start recording, and type a long sentence. Compare the render count against the same test with it set to false. You should see noticeably more renders when it’s on, which confirms the performance tradeoff described earlier in this guide is real and not just theoretical.

Common React Autosave Pitfalls

  • Relying on keyup for save triggers. It fires on non-content-changing keys and misses toolbar-driven changes like pasted images or applied formatting.
  • Skipping the debounce. Without it, you’ll flood localStorage and your API with redundant writes during normal typing.
  • Forgetting to clear the debounce timer on unmount. Leaving a setTimeout running after the component unmounts can throw errors or leak memory. Always clean up in your useEffect return function.
  • Not handling localStorage write failures. Storage can be full or disabled in private browsing. Wrap writes in a try/catch so a storage failure doesn’t crash your save flow.
  • Overwriting the server draft without asking. Always let the user choose between the local draft and the server version instead of picking one automatically.
  • Turning on immediateReactModelUpdate “just in case.” It’s rarely needed for autosave and adds performance cost you don’t need to pay.

Froala React Autosave FAQ

Does contentChanged fire for programmatic changes, like setContent() calls?

It can, depending on how the content is set. If you’re setting content through editor.html.set(), test this in your specific version, since behavior can vary. Setting the React model directly through model typically won’t re-trigger contentChanged on the editor instance itself.

How often should I sync to my API?

Every 5 to 10 seconds is a reasonable starting point for most text-heavy apps. Adjust based on your backend’s tolerance for request volume and how critical near-real-time durability is for your use case.

What if the user is offline when autosave tries to hit the API?

The fetch call will fail, and your catch block sets saveStatus to 'error'. Because hasUnsavedChangesRef.current stays true, the next interval tick will retry automatically once the connection returns.

Should I debounce the localStorage write too, or save on every keystroke?

Debouncing it, even with a short delay like 800ms, is still worth it. localStorage writes are synchronous and can block the main thread briefly on large documents, so avoiding a write on every single keystroke keeps typing feeling smooth.

Conclusion

You now have a complete autosave and draft recovery system for Froala Editor in React. It saves locally in near real time, syncs to your server on a controlled interval, shows users a clear save status, and gives them the choice to restore or discard a recovered draft. None of this required any exotic dependencies, just Froala’s contentChanged event, a couple of useRef timers, and standard browser APIs.

For more on Froala’s event system and React-specific configuration options, check the official Froala React documentation.

Try wiring this into your own project and see how it feels. If you want to go further, the same debounce pattern works well for syncing to collaborative editing backends or version history systems built on top of Froala.

graphical user interface, text

Vue WYSIWYG Editor v-model, Events, and Two-Way Binding Explained

Vue WYSIWYG Editor v-model Two-Way Binding Illustration

Integrating a standard form field into Vue is simple: add v-model, connect it to reactive state, and Vue keeps the interface and data synchronized. A WYSIWYG editor is more complex because it manages formatted HTML, toolbar actions, pasted content, selection state, and other editing behavior within its own interface.

This guide explains how to keep a Vue WYSIWYG editor synchronized with application state using v-model, component events, and Vue 3’s Composition API. Using the Froala Vue Rich Text Editor as the implementation example, you will learn how two-way binding works, how to respond to editor events, and how to update content without relying on manual DOM manipulation.

Key Takeaways

  • Use v-model to establish a single source of truth between your Vue application and the editor, ensuring changes stay synchronized in both directions.
  • Combine v-model with editor events such as contentChanged, focus, and blur to implement features like autosave, validation, live previews, and draft management.
  • Leverage Vue’s Composition API with ref() and reactive() to manage editor content alongside the rest of your application’s reactive state.
  • Update the bound reactive variable instead of manipulating the DOM, allowing Vue and the editor to handle synchronization automatically.
  • Follow performance best practices by debouncing expensive operations, keeping reactive state lightweight, and avoiding unnecessary re-renders.
  • Choose a Vue-native editor that integrates seamlessly with Vue’s reactivity system, provides a comprehensive event API, and supports the customization needed for production-ready applications.

Understanding Two-Way Binding in Vue

Two-way binding keeps your application’s state and UI synchronized. When the user updates an input, the underlying data changes automatically. Likewise, when the data changes programmatically, the UI reflects the new value.

With standard form elements, Vue makes this easy using the v-model directive.

<script setup>
import { ref } from 'vue'

const title = ref('')
</script>

<template>
  <input v-model="title" placeholder="Enter a title" />

  <p>Current value: {{ title }}</p>
</template>

When the user types into the input:

Input
   ↓
v-model
   ↓
title (ref)
   ↓
Vue re-renders the UI

This works because v-model combines two operations:

  • It passes the current value to the component.
  • It listens for updates and writes them back to the reactive variable.

Without v-model, you would need to handle both steps yourself.

<input
  :value="title"
  @input="title = $event.target.value"
/>

Although both examples produce the same result, v-model is cleaner and easier to maintain.

Rich text editors are different from native inputs. Instead of editing plain text, they manage formatted HTML, toolbar interactions, and editing state internally. As a result, they require an integration that synchronizes the editor with Vue’s reactive data instead of relying on a standard <input> element.

Why WYSIWYG Editors Need Special Binding

Unlike a textarea, a WYSIWYG editor is a complete application running inside your Vue component. It maintains its own editing environment while Vue manages your application’s state.

For example, when a user clicks Bold, the editor doesn’t simply append text. It updates the HTML, preserves the cursor position, records an undo history entry, refreshes the toolbar, and re-renders the content.

User Action
      ↓
Editor Internal State
      ↓
Generated HTML
      ↓
Vue Reactive State

A rich text editor typically manages:

  • The editable DOM
  • HTML content
  • Text formatting
  • Toolbar state
  • Cursor and selection
  • Clipboard operations
  • Undo/redo history

If the editor and Vue state become unsynchronized, problems quickly appear. For example, loading an existing article by updating a Vue variable won’t update the editor unless the integration propagates the new value. Likewise, changes made inside the editor won’t be available when the form is submitted unless they are synchronized back to Vue.

A Vue-compatible editor solves this by keeping both sides in sync automatically, allowing the editor to handle editing while Vue manages the application’s reactive state.

Setting Up Froala as a Vue WYSIWYG Editor

Froala provides an official Vue package that makes it straightforward to integrate a rich text editor into Vue 3 applications.

Install the required packages

Install the Froala editor and its Vue wrapper using your preferred package manager.

npm install froala-editor vue-froala-wysiwyg

Import the editor assets

Import the Froala JavaScript bundle to register the built-in editor plugins, then import the required styles.

import 'froala-editor/js/plugins.pkgd.min.js'
import 'froala-editor/css/froala_editor.pkgd.min.css'
import 'froala-editor/css/froala_style.min.css'

Register the Vue plugin

Register the Froala plugin in your application’s entry file.

import { createApp } from 'vue'
import App from './App.vue'

import VueFroala from 'vue-froala-wysiwyg'

createApp(App)
  .use(VueFroala)
  .mount('#app')

Once registered, the <froala> component is available throughout your application.

Create your first editor

Create a reactive variable to hold the editor content and bind it to the editor component.

<script setup>
import { ref } from 'vue'

const content = ref('<p>Hello, Vue!</p>')
</script>

<template>
  <froala v-model:value="content" />
</template>

Running the application displays a fully functional rich text editor with the initial HTML stored in content. At this point, the editor is installed and ready to use. In the next section, you’ll learn how the v-model directive keeps the editor and Vue’s reactive state synchronized automatically.

Binding the Editor with v-model

The v-model directive connects the Froala editor to a reactive variable, creating a single source of truth for your editor content.

<froala v-model:value="content" />

Whenever the user edits the document, Vue automatically updates the content variable. Likewise, if you update content in your application, the editor immediately reflects the new value.

content (ref)
      ⇅
   v-model
      ⇅
 Froala Editor

For example, after loading an existing article from an API, simply assign the returned HTML to the reactive variable.

content.value = response.data.body

The editor immediately displays the updated content without requiring any additional synchronization code.

Because content is reactive, other parts of your application always receive the latest editor content.

<p>Content length: {{ content.length }}</p>

<button :disabled="!content">
  Publish
</button>

As the user edits the document, the character count and button state update automatically because they depend on the same reactive variable.

This is the primary benefit of using v-model. Instead of manually reading HTML from the editor or writing content back into it, you work with a single reactive variable while Vue and Froala handle the synchronization for you. The next section explores how these updates propagate through the rest of your Vue application.

Understanding the Data Flow

When you bind Froala using v-model, the editor becomes part of Vue’s reactive data flow.

User Types
      ↓
Froala Editor
      ↓
v-model
      ↓
Vue State
      ↓
Other Components
      ↓
Editor Updates

Here’s what happens at each step:

  1. User Types
    The user edits content inside the Froala editor.
  2. Froala Editor
    Froala updates its internal document model, toolbar state, cursor position, and generated HTML.
  3. v-model
    The updated HTML is automatically assigned to your reactive variable.
  4. Vue State
    Your ref() or reactive() object now contains the latest editor content.
  5. Other Components
    Any component using that reactive data is updated automatically.
  6. Editor Updates
    If the reactive value changes elsewhere (for example, after loading a draft), Vue updates the editor to display the new content.

The following example demonstrates this flow.

<script setup>
import { ref } from 'vue'

const content = ref('<p>Hello World!</p>')
</script>

<template>
  <froala v-model:value="content" />

  <h3>Preview</h3>

  <div v-html="content"></div>
</template>

As the user types, the preview updates automatically because both the editor and preview share the same reactive variable.

Listening to Editor Events

While v-model synchronizes content, editor events let you react to user actions.

Some commonly used Froala events include:

EventCommon Use
initializedRun setup logic after the editor loads
contentChangedAutosave, validation, live preview
focusHighlight the active editor
blurValidate or save content
keyupCharacter or word counter
keydownKeyboard shortcuts
paste.afterSanitize pasted content
image.insertedTrack uploaded images
image.removedClean up deleted images

For example, you can listen for content changes to trigger an autosave.

<template>
  <froala
    v-model:value="content"
    :config="config"
  />
</template>

<script setup>
import { ref } from 'vue'

const content = ref('')

const config = {
  events: {
    contentChanged() {
      console.log('Saving draft...')
    }
  }
}
</script>

You can also monitor when the editor gains or loses focus.

const config = {
  events: {
    focus() {
      console.log('Editor focused')
    },

    blur() {
      console.log('Editor lost focus')
    }
  }
}

Character counting is another common use case.

const config = {
  events: {
    keyup() {
      console.log('Content updated')
    }
  }
}

Instead of polling for changes, your application responds immediately whenever an event occurs.

Combining v-model with Editor Events

v-model and editor events solve different problems.

  • v-model stores the current editor content.
  • Events notify your application when something happens.

In most real-world applications, you’ll use both together.

<script setup>
import { ref } from 'vue'

const content = ref('')
const hasUnsavedChanges = ref(false)

const config = {
  events: {
    contentChanged() {
      hasUnsavedChanges.value = true
    }
  }
}
</script>

<template>
  <froala
    v-model:value="content"
    :config="config"
  />

  <button :disabled="!hasUnsavedChanges">
    Save Draft
  </button>
</template>

Here:

  • v-model keeps content synchronized.
  • contentChanged enables the Save Draft button.
  • After saving successfully, you can simply reset the flag.
hasUnsavedChanges.value = false

The same approach works for many common features:

  • Enable or disable the Publish button.
  • Display a live preview.
  • Show an “Unsaved changes” warning.
  • Trigger autosave every few seconds.
  • Enforce character or word limits.

A typical workflow looks like this:

User edits content
        ↓
contentChanged event
        ↓
Application logic runs
        ↓
v-model updates content
        ↓
UI refreshes automatically

Working with Reactive Data

Vue 3’s Composition API uses ref() and reactive() to create reactive state.

For editor content, ref() is usually the simplest choice.

<script setup>
import { ref } from 'vue'

const content = ref('')
</script>

Bind it directly to the editor.

<froala v-model:value="content" />

Whenever the user edits the document:

User types
      ↓
content.value updates
      ↓
Vue reactivity runs
      ↓
Dependent components update

If your editor belongs to a larger form, reactive() can group related fields together.

<script setup>
import { reactive } from 'vue'

const article = reactive({
  title: '',
  content: '',
  category: ''
})
</script>

<template>
  <input v-model="article.title" />

  <froala v-model:value="article.content" />
</template>

Now the title, editor content, and category all belong to the same reactive object.

Resetting and Updating Editor Content

Since the editor is bound with v-model, updating the reactive variable automatically updates the editor.

Load an existing article

content.value = article.body

Switch to another document

content.value = selectedPost.content

Clear the editor

content.value = ''

Load a template

content.value = `
<h2>Meeting Notes</h2>

<ul>
  <li></li>
</ul>

There’s no need to call DOM APIs or manually replace the editor’s HTML. Simply update the reactive value, and Vue synchronizes the editor.

For best results:

  • Always update the bound reactive variable.
  • Avoid manually manipulating the editor’s DOM.
  • Keep a single source of truth for editor content.
  • Let v-model handle synchronization.

Using the Editor Inside Vue Forms

Rich text editors are often used alongside other form fields.

<script setup>
import { reactive } from 'vue'

const form = reactive({
  title: '',
  content: ''
})
</script>

<template>
  <form>

    <input
      v-model="form.title"
      placeholder="Article title"
    />

    <froala v-model:value="form.content" />

  </form>
</template>

Your form data now contains both the title and rich text content.

A typical submission looks like this:

async function submit() {

  if (!form.content.trim()) {
    alert('Content is required.')
    return
  }

  await api.post('/articles', form)
}

The workflow is straightforward:

Editor
    ↓
Form Model
    ↓
Validation
    ↓
Submit

Because the editor is part of the same reactive form model, validation, submission, and API requests work exactly like any other Vue form field. This keeps your forms simpler while ensuring the editor content always stays synchronized with the rest of your application.

Get the complete Vue 3 and Froala example from GitHub to explore `v-model`, editor events, live preview, autosave, programmatic content updates, and form validation in a runnable project.

Common Mistakes Developers Make

Integrating a Vue WYSIWYG editor is straightforward once you understand Vue’s reactivity model. The following mistakes are common and can lead to synchronization issues or unnecessary complexity.

Manipulating the DOM directly

Avoid updating the editor by manually changing its HTML.

❌ Avoid

document.querySelector('.fr-element').innerHTML = html

✅ Use

content.value = html

Always update the reactive variable and let v-model synchronize the editor.

Not using v-model

Some developers manually read the editor content before submitting a form.

❌ Avoid

const html = editor.html.get()

✅ Use

<froala v-model:value="content" />

The latest HTML is always available in content.value.

Mutating HTML incorrectly

Avoid modifying the generated HTML using string operations.

❌ Avoid

content.value += '<p>New text</p>'

Instead, update the editor naturally through user interaction or replace the content with a complete HTML string when appropriate.

Ignoring the editor lifecycle

Some logic should only run after the editor has finished initializing.

const config = {

  events: {

    initialized() {

      console.log('Editor is ready')

    }

  }

}

Waiting for the initialized event helps avoid timing issues.

Updating state incorrectly

Always modify the reactive variable instead of trying to synchronize multiple copies of the same data.

❌ Avoid

let editorContent = ''

let html = ''

✅ Use

const content = ref('')

Maintain a single source of truth.

Recreating the editor unnecessarily

Avoid destroying and recreating the editor whenever content changes.

❌ Avoid

editor.destroy()

editor.initialize()

Instead, simply update the bound value.

content.value = article.body

Binding large objects instead of HTML

The editor should be bound to the HTML content, not the entire article object.

❌ Avoid

const article = reactive({

  title: '',

  author: '',

  content: '',

  tags: []

})




<froala v-model:value="article" />

✅ Use

<froala v-model:value="article.content" />

This keeps updates focused and avoids unnecessary reactive work.

Performance Best Practices

Most applications won’t experience performance issues, but following a few best practices helps keep the editor responsive as projects grow.

Avoid unnecessary re-renders

Keep the editor mounted whenever possible. If only the content changes, update the reactive variable instead of recreating the component.

content.value = article.body

Lazy load the editor

If the editor appears only on certain pages, consider loading it only when needed.

const Editor = defineAsyncComponent(() =>

  import('./Editor.vue')

)

This reduces your application’s initial bundle size.

Debounce autosave

Saving on every keystroke can generate unnecessary API requests.

import { debounce } from 'lodash-es'




const saveDraft = debounce(() => {

  console.log('Saving...')

}, 1000)

Use the debounced function inside the contentChanged event.

Keep event handlers lightweight

Event handlers fire frequently, especially keyup and contentChanged.

Prefer:

contentChanged() {

  saveDraft()

}

Avoid expensive calculations or multiple API calls inside every event.

Keep reactive state lightweight

Store only the editor’s HTML in the reactive variable.

const content = ref('')

Avoid storing editor instances or unrelated application data inside the same reactive value.

Consider large documents

Very large articles naturally require more processing.

For better performance:

  • Update only the bound HTML.
  • Debounce expensive operations.
  • Avoid unnecessary watchers.
  • Use editor events only where needed.

These small optimizations help maintain a smooth editing experience.

Why Froala Works Well with Vue Reactivity

Vue’s reactive data model works best with components that follow the same principles. Froala’s official Vue integration is designed to fit naturally into that workflow.

Some of its key capabilities include:

  • Native Vue component integration
  • Simple v-model support for two-way binding
  • Comprehensive event system for responding to user actions
  • Automatic synchronization with Vue’s reactive state
  • Fast rendering for responsive editing
  • Extensive configuration and toolbar customization
  • A plugin ecosystem for adding features such as tables, images, markdown, file management, and more

Because the editor integrates directly with Vue’s reactivity system, developers can work with familiar Composition API patterns instead of manually synchronizing editor content. This results in cleaner code and makes it easier to build features such as autosave, live previews, form validation, and collaborative editing while keeping the application’s state consistent.

Conclusion

Vue’s v-model makes two-way binding simple by keeping application state and the user interface synchronized automatically. Rich text editors introduce additional complexity because they manage their own editing environment, making proper synchronization essential for a reliable editing experience.

By combining v-model with editor events, you can build responsive features such as autosave, live previews, validation, and draft management while keeping your code clean and maintainable. Using a Vue-native editor like Froala allows you to integrate rich text editing into modern Vue 3 applications without sacrificing the benefits of Vue’s reactive data model.

Ready to build a seamless editing experience? Try the Froala Vue Rich Text Editor and start integrating rich text editing into your Vue 3 applications today.

FAQ

How do I bind a Vue WYSIWYG editor using v-model?

Bind the editor component to a reactive variable using Vue’s v-model directive. As users edit the content, the editor automatically updates the reactive variable. Likewise, any programmatic changes to that variable are reflected in the editor, keeping both synchronized.

Why doesn’t a rich text editor behave like a normal input?

Unlike native inputs, a WYSIWYG editor manages its own editable DOM, formatting, toolbar interactions, cursor position, and undo/redo history. Because of this additional functionality, it requires an integration layer to synchronize its content with Vue’s reactive data model.

Can I use editor events together with v-model?

Yes. v-model keeps the editor’s content synchronized with your application state, while editor events let you respond to user actions such as content changes, focus, blur, keyboard input, image uploads, and paste operations. Using both together makes it easy to implement features like autosave, validation, and live previews.

How do I update editor content programmatically?

Simply update the reactive variable bound to the editor with v-model. Vue automatically propagates the new value to the editor, making it easy to load existing articles, switch between documents, clear the editor, or insert predefined templates.

Is Froala compatible with Vue 3?

Yes. Froala provides an official Vue integration that supports Vue 3, including seamless v-model binding, reactive state synchronization, and a comprehensive event API for building modern rich text editing experiences.

 

Customize Froala AI Assist with Tone and Translation Presets

customize Froala AI Assist

Out of the box, Froala AI Assist provides generic options for “Tone” and “Translation” dropdowns. They work fine for demos, but these lack the nuance required for enterprise applications.

Your users expect tools that match their specific workflow, whether that means enforcing strict legal compliance, adopting a playful brand voice, or translating into languages relevant to their region.

The Froala AI Assist plugin allows you to customize these presets completely through configuration. By leveraging aiAssistToneOptions and aiAssistTranslateOptions, you can build a dynamic, context-aware editing experience without modifying the core library code.

Key Takeaways

  • You can replace default tone presets with brand-specific instructions using aiAssistToneOptions.
  • Translation targets are fully customizable via aiAssistTranslateOptions, allowing for regional or industry-specific language lists.
  • Dynamic configurations allow you to populate options programmatically based on user roles or browser locales.
  • Proper prompt engineering within these options ensures the AI preserves HTML structure while altering content style.

customize AI editor

Why Generic Presets Break Down

Consider a few real scenarios:

  • A law firm uses “Professional” to rewrite a contract clause. The AI returns friendly, approachable language—the exact opposite of what legal writing demands. The lawyer now has to edit it back to formal precision, defeating the purpose.
  • A SaaS onboarding team uses “Casual” for help documentation. The AI injects slang and contractions. It works for the hero’s journey marketing copy, not for someone trying to reset their password at 2 AM.
  • A global marketplace shows all 50 languages to every user. A Spanish speaker in Madrid sees “Translate to Mandarin” as an option. They never use it. The dropdown bloats. Discoverability suffers.

These aren’t edge cases. These are the daily reality of shipping AI tools to real teams.

How It Works: The Configuration Structure

Froala gives you two configuration keys to customize tone and translation presets:

  1. aiAssistToneOptions: Replace tone buttons with your own.
  2. aiAssistTranslateOptions: Replace translation buttons with your own.

Both use the same simple structure: an array of objects. Each object has two properties:

  1. title: The string displayed to the user in the UI.
  2. prompt: The instruction sent to your AI backend. This tells the model how to rewrite or translate the selected text.

Understanding this structure is the first step to learning how to customize AI tone editor behaviors effectively.

Real Example For Creating Brand-Specific Tones: A Law Firm

Let’s build this concretely. Imagine you’re building a CMS for a law firm. “Friendly” is a liability. “Formal” is too vague. You need precise, distinct options.

Step 1: Define Your Tone Objects

Create an array with tone presets tailored to your workflow:

const legalTonePresets = [
  {
    title: 'Legally Precise',
    prompt: 'Rewrite the following content using precise, unambiguous legal terminology. Ensure all liabilities are clearly stated. Avoid casual language or hedging. Preserve all HTML formatting, links, and styles.'
  },
  {
    title: 'Client-Friendly',
    prompt: 'Rewrite the following content in plain English suitable for non-lawyer clients. Simplify complex jargon but retain accuracy. Use short sentences. Preserve all HTML formatting, links, and styles.'
  },
  {
    title: 'Internal Memo',
    prompt: 'Rewrite the following content as a concise internal memo. Focus on action items and deadlines. Use bullet points if helpful. Preserve all HTML formatting, links, and styles.'
  }
];

Notice what’s happening here: you’re not asking for “professional” tone. You’re specifying the exact linguistic goal for each button. “Unambiguous legal terminology” is different from “plain English.” The AI can now target distinct stylistic clusters instead of guessing what you mean by “professional.”

Step 2: Inject Into Froala Config

Pass this array into the initialization:

new FroalaEditor('#editor', {
  pluginsEnabled: ['aiAssist'],
  aiSupplementalTermsAccepted: true,
  
  // Replace default tones with your legal presets
  aiAssistToneOptions: legalTonePresets,
  
  // Your API endpoint
  aiAssistEndpoint: 'https://your-api.com/rewrite'
});

Step 3: What Actually Changes

Now when a lawyer opens the “Edit Smarter” dropdown, they see three buttons: “Legally Precise,” “Client-Friendly,” and “Internal Memo.” Each one produces output calibrated to a specific use case. The AI no longer guesses. The lawyer no longer edits the AI’s output. The document stays correct.

Going Deeper: Role-Based Customization

The pattern scales. You can generate different tone presets based on user role:

function getToneOptionsForRole(userRole) {
  const allTones = {
    junior_editor: [
      { title: 'Proofread', prompt: '...' },
      { title: 'Simplify', prompt: '...' }
    ],
    senior_editor: [
      { title: 'Proofread', prompt: '...' },
      { title: 'Simplify', prompt: '...' },
      { title: 'Legal Review', prompt: '...' },
      { title: 'Executive Summary', prompt: '...' }
    ]
  };
  
  return allTones[userRole] || allTones.junior_editor;
}

Junior editors see safe, basic options. Senior editors see the full toolkit. The same AI Assist plugin powers both experiences. One configuration, multiple implementations.

Common Pitfalls to Avoid

Ignoring HTML Preservation

If you omit “Preserve all HTML formatting” from your prompts, the AI may return plain text. When Froala inserts this back into the editor, all styling, links, and embedded media will disappear. Always include this instruction in every prompt string.

Overly Complex Prompts

Keep the prompt field focused. If you need extensive rules, consider moving them to your backend’s system prompt rather than cluttering the client-side config. The client-side prompt should handle the immediate transformation task.

Hardcoding Sensitive Data

Never embed API keys or secret tokens in aiAssistHeaders on the client side. Use a proxy server or ensure your CORS policy restricts access to trusted domains.

Conclusion

The flexibility of aiAssistToneOptions and aiAssistTranslateOptions transforms Froala AI Assist from a generic utility into a tailored productivity tool. By defining precise prompts and generating options dynamically, you ensure that the AI behaves consistently with your brand standards and user expectations.

Start by auditing your current content workflows. Identify the recurring style adjustments your team makes manually, then encode those patterns into custom tone presets. The result is a faster, safer, and more consistent editing experience.

Read the full AI Assist Plugin documentation to explore additional configuration options like aiAssistPromptTemplate and aiAssistResponseParserPath.

Frequently Asked Questions

Can I add icons to my custom tone buttons?

Currently, the AI Assist dropdown renders text-only titles for tone and translation options. Icons are reserved for standard toolbar buttons. To improve discoverability, use clear, descriptive titles like “Make Professional” or “Translate to FR.”

Do these options work with React, Vue, or Angular wrappers?

Yes. The aiAssistToneOptions and aiAssistTranslateOptions are part of the core Froala configuration object. Pass them into the config prop or equivalent initialization method in your framework-specific wrapper component.

Can I mix default and custom tones?

Yes. You can retrieve the default options and concatenate your custom ones. However, it is generally cleaner to define a complete set of curated options to maintain a consistent UX and avoid confusing duplicates.

What happens if the AI fails to preserve HTML?

If the AI strips tags, the editor will render plain text. To mitigate this, ensure your backend system prompt reinforces HTML preservation. You can also use the aiAssistRequest custom function to sanitize or repair the response before returning it to Froala.
graphical user interface, text

Accessible Angular WYSIWYG Editor for Keyboard and Screen Readers

An illustration of a browser window showing an Angular WYSIWYG editor toolbar with Bold, Italic, and Link buttons. The Italic button is highlighted in green, and a dotted path traces the keyboard navigation route from the toolbar through to the editor's text area below.

Search for Angular WYSIWYG editor guides, and you’ll find plenty on installation. From there, writers branch into SSR configuration, standalone components, and toolbar customization. Accessibility, however, gets a bullet point at best.

This matters because making a rich text toolbar keyboard- and screen reader-accessible is no easy task. For instance, icon-only buttons, dynamic state, and nested menus create a layer of complexity that a simple form seldom faces.

This guide discusses what WCAG-level keyboard navigation and screen reader support actually require from an Angular rich text editor implementation. So, instead of just a general accessibility overview, you’ll get a focused, technical look at the accessibility layer specifically.

Note: If you’re evaluating editors more broadly, the best WYSIWYG editor for Angular apps covers that question separately.

Key Takeaways

  • Rich text toolbars require explicit ARIA labeling and keyboard handling that standard form inputs do not.
  • Failing to reactively bind ARIA attributes can cause Angular’s change detection to silently break screen reader state.
  • Automated tools like axe and Lighthouse catch structural issues but can’t fully validate toolbar keyboard flow or screen reader announcements.
  • Focus management during dialogs (e.g., link insertion or image upload) is a frequent accessibility failure point in Angular WYSIWYG editors.
  • Building for accessibility early can significantly help reduce effort costs.

Why Rich Text Toolbars Are Hard to Make Accessible

A standard text input, having a label, role, and value, is simple for a screen reader. In contrast, a WYSIWYG toolbar is a collection of buttons, most of which are icon-only, arranged in groups, and with dynamic states. Because of this, making one accessible could take more effort.

The first problem is icon-only buttons; for example, a “Bold” button displays a ‘B’ icon. Without an aria-label attribute on that button element, a screen reader announces it as “button” with no further context. That tells screen reader users nothing about what the button does.

Note: aria-label is an HTML attribute that overrides what a screen reader announces for an element. For an icon button, adding aria-label=”Bold” makes the screen reader say “Bold, button” instead of just “button.”

The next problem concerns the visual-only state. When a user selects bold text and clicks the “Bold” button, it visually activates, changing color or appearing raised. If the underlying button element doesn’t carry an aria-pressed=”true” attribute, screen reader users have no way to know that the formatting is active.

The third problem concerns nested menus, like when a font size dropdown or paragraph format selector introduces nested interactive elements. Without proper keyboard handling, a user pressing “Tab” to navigate the toolbar will skip over the dropdown’s contents unpredictably. This can break the keyboard tab order and leave parts of the toolbar unreachable without a mouse.

Each of these problems requires deliberate implementation. Some teams solve them manually, while others start with an editor that handles them. Either way, addressing these concerns early can make a significant impact on accessibility, usability, and compliance.

Keyboard Navigation Requirements

A Froala rich text editor in a browser window showing text being selected and formatted using only keyboard shortcuts, with no mouse interaction.

A keyboard-accessible Angular WYSIWYG editor must support full toolbar operability without a mouse. That means letting users press “Tab” to reach the toolbar and the arrow keys to navigate between buttons. Similarly, “Enter” should activate the focused button, and “Tab” should exit the toolbar and return focus to the editing area.

This pattern follows the ARIA authoring practices guide’s “toolbar” widget pattern. The key concept is that a toolbar behaves like a single tab stop (instead of a series) for every button. Once focus enters the toolbar, arrow keys handle internal navigation. This prevents users from needing to tab through numerous toolbar buttons before reaching the editor.

Tip: If you’re auditing your current implementation, try navigating the entire editor using only the “Tab,” arrow, “Enter,” and “Escape” keys. If you can’t reach every toolbar action without a mouse, then the keyboard support is incomplete.

Keyboard shortcuts add complexity; an editor might assign “Ctrl+B” to “Bold,” but a screen reader might intercept the same keystroke. The solution is to not shy away from shortcuts but to test them explicitly with an active screen reader. NVDA on Windows and VoiceOver on macOS are the two most important to test, since they intercept keys differently.

Focus management during dialogs is one of the most commonly broken areas in Angular WYSIWYG editors. When a user triggers the link insertion dialog, focus must move into that dialog. Likewise, when the user closes it, focus must return to the toolbar button that opened it. Without this behavior, keyboard users lose their place in the document entirely after every modal interaction.

Note: This pattern is called “focus trapping.” It means that while a dialog is open, “Tab” keypresses should cycle only through elements within that dialog. When the dialog closes, focus returns to the triggering element.

Screen Reader Support for Toolbar and Content

Screen reader support goes beyond adding aria-label attributes to buttons. It requires a coherent or “announced” experience for every action a user might take.

At the very least, toolbar buttons need a label and a state. The label tells the user what the button does, while the state tells the user what the button currently is.

For toggle buttons like “Bold,” “Italic,” and “Underline,” the correct ARIA attribute is aria-pressed. When “Bold” is inactive, the button carries the aria-pressed=”false” attribute (and the aria-pressed=”true” attribute otherwise). A screen reader then announces “Bold, button, not pressed” or “Bold, button, pressed” accordingly.

For buttons that open menus, the correct attribute is aria-expanded. A font family dropdown button should carry aria-expanded=”false” when collapsed and aria-expanded=”true” when open. The menu itself should carry role=”menu”, and each option inside it should carry role=”menuitem”.

Content changes also need announcements. When a user inserts a table or an image, a screen reader should confirm that something changed. To do this, use an ARIA live region, a visually hidden element that a screen reader monitors for text changes.

When the editor inserts a table, a short message like “Table inserted” populates the live region. Afterwards, the screen reader announces it without interrupting the user’s flow.

An illustration of a browser window with a WYSIWYG editor toolbar and a glowing table inserted in the content area. An arrow flows from the table to a ghost element outside the browser, then up to a green speech bubble, representing how an ARIA live region announces content changes to a screen reader.

Testing with a real screen reader is not optional. Automated linters like axe may catch missing labels and contrast ratios. However, they don’t test whether the sequence of announcements makes sense to a real user navigating a toolbar. For WYSIWYG toolbars, manual screen reader testing with NVDA or VoiceOver reveals issues that no automated tool surfaces.

You can also look at how an accessible rich text comment system in React handles similar patterns. The framework differs, but the ARIA approach maps directly.

Angular-Specific Implementation Considerations

Angular introduces two considerations that are not present in a vanilla JavaScript editor integration. The first is reactive ARIA binding, and the second is change detection timing.

In a keyboard-accessible Angular WYSIWYG editor, toolbar state changes constantly. For example, the user selects text, and the “Bold” button should update its aria-pressed state. If the user moves the cursor into a heading, the paragraph format selector should reflect that.

These updates need to happen through Angular’s binding system, not through manual DOM manipulation. The correct pattern uses property binding to keep ARIA attributes in sync with component state. Here’s what a simplified example of that looks like:

<!--In your template-->
<button
    [attr.aria-pressed]="isBoldActive"
    (click)="toggleBold()"
    aria-label="Bold">
    <span class="fr-icon" aria-hidden="true">B</span>
</button>

Note the aria-hidden=”true” on the icon span. This tells the screen reader to ignore the icon element entirely, since the parent button already has an aria-label that communicates its purpose. Without this, a screen reader might announce both the label and the icon text.

// In your Angular component
export class EditorToolbarComponent {
    isBoldActive = false;
    onEditorStateChange(editor: any) {
          this.isBoldActive = editor.format.is('bold');
    }
}

The [attr.aria-pressed] binding only reflects the correct state if the component property from which it reads actually updates. This happens inside the editor’s state-change callback, where you query the editor instance for the current format and assign it to the property. Without that callback, the binding has nothing accurate to read.

The second consideration concerns change detection. Under Angular’s default strategy, bindings re-evaluate frequently enough that ARIA attributes stay accurate. But OnPush change detection only re-evaluates when a component’s inputs change.

An editor state change coming from a plugin or internal event doesn’t count as an input change. Thus, the binding never re-runs, and the ARIA attribute goes stale.

Tip: If you use OnPush change detection and notice that ARIA states lag behind the visual state, call ChangeDetectorRef.markForCheck() inside the editor’s state-change callback. This forces a re-evaluation.

For the base Angular integration on top of which this accessibility layer builds, refer to the “how to integrate Froala with Angular” guide.

What to Test before Calling It Accessible

Accessibility testing for an Angular WYSIWYG editor requires both an automated pass and a manual one.

An illustration of a friendly robot scanning a browser window with a magnifying glass on the left, and a human hand pressing a highlighted key on a keyboard on the right, representing automated and manual accessibility testing.

Start by running automated tools like axe and Lighthouse against your editor page. These tools check for missing aria-label attributes, insufficient color contrast, invalid ARIA roles, and other structural problems. Then, resolve every issue they flag before moving to manual testing.

Automated checks are fast and catch the easy failures, but they can’t evaluate flow or logic. So, you should also do a full keyboard-only walkthrough. Close your browser DevTools, unplug your mouse, and navigate the entire editor using only the keyboard. Then,

  • Verify that arrow keys move between toolbar groups.
  • Verify that “Enter” activates buttons.
  • Open the link insertion dialog and confirm whether focus moves into it.
  • Close the dialog and confirm whether focus returns to the toolbar.
  • Open the image upload dialog and repeat.

If any step above requires a mouse, then the keyboard support is incomplete.

Tip: Do this walkthrough on both Windows and macOS, since keyboard focus behavior can differ between OSs.

Finally, do a screen reader walkthrough. Enable NVDA on Windows or VoiceOver on macOS. Navigate the toolbar and verify that every button announces its label and state. Insert a table and an image, and do the same verification.

Where This Fits into a Broader Angular Accessibility Story

An accessible Angular WYSIWYG editor is one component in a larger accessibility effort. The editor handles content creation, while the rest of the application handles navigation, forms, modals, and dynamic content. These need consistent treatment, and a few principles apply across all of them.

First, focus management should remain predictable throughout the app instead of just within the editor. Furthermore, ARIA live regions should announce significant state changes across the application, including the editor toolbar. Color contrast ratios should also meet WCAG AA minimums on every screen.

From a vendor perspective, understanding the level of built-in accessibility an editor provides matters. Froala’s commitment to the Section 508 standard documents the baseline accessibility posture with which the editor ships. Knowing this baseline helps teams understand what an editor covers versus what they need to implement themselves.

Note: Section 508 is a U.S. law requiring federal agencies and their vendors to make electronic and information technology accessible to people with disabilities. WCAG (Web Content Accessibility Guidelines) is the international technical standard most accessibility work references. Section 508 and WCAG 2.0 AA requirements overlap significantly.

For a broader view of accessibility best practices beyond the editor, read 9 simple ways to increase your website’s accessibility.

All in all, rich text editor accessibility is not a one-time task. It requires ongoing testing as the editor library updates, as Angular upgrades introduce change detection changes, and as browser and screen reader behavior evolves. Building it correctly from the start is significantly less expensive than auditing and retrofitting a production implementation later.

Conclusion

A working Angular WYSIWYG editor and an accessible one are not the same thing. Installation, SSR setup, and toolbar customization solve one set of problems. Keyboard navigation, ARIA labeling, screen reader announcements, and focus management solve another. If you’re integrating an Angular rich text editor into a production application, you need to solve both sets of problems.

In your implementation, bind ARIA attributes reactively, manage focus through dialogs, and use live regions for content change announcements. Moreover, test with real screen readers rather than relying on automated linting alone.

The harder version is retrofitting accessibility onto a fully built editor integration. That’s when “it works visually” meets “it doesn’t work for 15% of your users.” With the right pattern and by planning from the start, you can solve Angular-specific considerations without requiring heroic effort.

Ready to improve your Angular editor’s keyboard and screen reader support? Explore the Froala Angular rich text editor and start building an accessible, configurable editing experience for your application.

Frequently Asked Questions (FAQs)

Why is a rich text editor toolbar harder to make accessible than a standard form?

Toolbars typically use icon-only buttons, have visual-only active/disabled states, and often include nested dropdowns. All these need explicit ARIA labeling and keyboard handling that a standard text input doesn’t require.

Does Angular’s change detection cause accessibility problems in a WYSIWYG editor?

It can, if ARIA attributes tied to toolbar state aren’t updated reactively. A button that visually shows “active” but whose aria-pressed attribute doesn’t update leaves screen reader users with incorrect information.

Is automated accessibility testing (like axe or Lighthouse) enough for a rich text editor?

No. Automated tools catch missing labels and contrast issues but can’t tell you whether keyboard navigation actually flows logically through a toolbar or whether a screen reader announces an inserted table sensibly. Manual testing with a real screen reader is necessary.

What keyboard navigation should an accessible Angular WYSIWYG editor support?

An accessible Angular WYSIWYG editor must support full toolbar operability via keyboard alone. This includes “Tab” to reach the toolbar, arrow keys to move between toolbar groups, and “Enter/Space” to activate. This also includes proper focus management when dialogs (like link or image insertion) open and close.

Should accessibility be added after the editor is built or designed in from the start?

You should build accessibility from the start where possible. Retrofitting ARIA labeling and keyboard handling onto an already-built toolbar is significantly more work than building it in alongside the initial implementation.

graphical user interface, text

Integrate Froala WYSIWYG Editor into Svelte

Integrate Froala WYSIWYG editor with Svelte

Adding a rich text editor to a Svelte app used to mean writing your own wrapper: mounting Froala in an onMount, tearing it down in onDestroy, and manually piping content in and out of your component state. Froala Editor v5.3.0 removes that work. The official svelte-froala-wysiwyg package gives you a real Svelte component with two-way binding, full access to Froala’s options and events, and direct access to the underlying editor instance.

This guide walks through installing the SDK, rendering the editor, wiring up two-way binding, configuring the toolbar, handling events, initializing on special tags, controlling the editor manually, and driving it programmatically through the editor instance. Every prop is explained, and every code sample is copy-paste ready.

Key Takeaways

  • The svelte-froala-wysiwyg component ships with Froala v5.3.0 and replaces hand-rolled Svelte integrations.
  • Two-way binding through bind:model keeps your editor content and a Svelte variable in sync with no extra glue code.
  • The config prop accepts the full Froala options object, including events, so you configure everything in one place.
  • bind:editor hands you the live editor instance so you can call any of Froala’s methods directly from your component.

Prerequisites

  • Node.js and npm installed.
  • A Svelte project (this guide uses Svelte 5, the current default). The wrapper also works with Svelte 3 and 4.
  • A Froala Editor license key for production. You can start with a free trial key.

Step 1: Install the SDK

Install the wrapper from npm:

npm install svelte-froala-wysiwyg

The froala-editor core package is a dependency of the wrapper, so you do not need to install it separately. That core package is where the editor’s JavaScript and CSS live, and you will import the CSS from it in the next step.

Step 2: Render the Editor and Wire Up Two-Way Binding

Here is the smallest working setup. Drop this into any .svelte file, for example src/App.svelte in a plain Vite + Svelte project.

<script>
  // Import the Froala wrapper component.
  import FroalaEditor from "svelte-froala-wysiwyg";

  // Load Froala's editor UI styles (toolbar, buttons, popups).
  import 'froala-editor/css/froala_editor.pkgd.min.css';

  // Load Froala's content styles (applied to the text you type).
  import 'froala-editor/css/froala_style.css';

  // Reactive state holding the editor's HTML.
  // $state() makes this a Svelte 5 rune so bind:model can update it.
  let htmlContent = $state("<p>Hello, Froala!</p>");
</script>

<!--
  bind:model creates a two-way link between htmlContent and the editor.
  Type in the editor and htmlContent updates. Change htmlContent in code
  and the editor updates. No event listeners required.
-->
<FroalaEditor bind:model={htmlContent} />

<!-- A live preview to prove the binding works. -->
<div class="preview">
  {@html htmlContent}
</div>

A few things worth explaining here, because they trip people up:

Why $state()? In Svelte 5, a plain let htmlContent = "..." is not reactive enough to be a two-way binding target. Wrapping the initial value in the $state() rune tells Svelte to track it. If you are on Svelte 3 or 4, use a plain let instead, exactly as the official docs show. The bind: directive itself works the same across all three versions, so the <FroalaEditor> line does not change.

Why {@html htmlContent}? Svelte escapes interpolated strings by default to prevent XSS. The editor produces real HTML, so you need {@html ...} to render it as markup rather than as escaped text. Only do this with content you trust or have sanitized. Rendering unsanitized user HTML is a security risk, which is a good reason to sanitize on your server before storing or re-displaying it.

Play with our interactive demo.

Step 3: Configure the Editor with the config Prop

Every Froala option goes through a single config prop, which takes a plain object. This is where you set the license key, the toolbar, the placeholder, and anything else from the Froala options reference. 

<script>
  import FroalaEditor from "svelte-froala-wysiwyg";
  import 'froala-editor/css/froala_editor.pkgd.min.css';
  import 'froala-editor/css/froala_style.css';

  let htmlContent = $state("");

  const myConfig = {
    // Your Froala license key. Standard across all Froala SDKs.
    // Omit during local trials; required for production.
    key: 'YOUR_FROALA_LICENSE_KEY',

    // Grey prompt text shown when the editor is empty.
    placeholderText: 'Start typing...',

    // Turn off the character counter in the bottom-right corner.
    charCounterCount: false,

    // Choose exactly which toolbar buttons appear, and in what order.
    toolbarButtons: ['bold', 'italic', 'underline']
  };
</script>

<FroalaEditor config={myConfig} bind:model={htmlContent} />

Breaking down each option:

  • key is your license key.
  • placeholderText sets the hint text shown in an empty editor. Set it to an empty string to hide the placeholder entirely.
  • charCounterCount toggles the character count display. It is true by default; set it to false to hide it.
  • toolbarButtons is an array of button command names, rendered left to right in the order you list them. Anything you leave out simply does not appear. The full list of available commands lives in the options documentation.

Defining configuration as a separate const keeps your markup clean and makes the config easy to reuse or test. Declare it outside any reactive block so it is created once rather than rebuilt on every render.

Step 4: Handle Editor Events

Froala events are not separate props. They live inside a nested events object on your config. Each key is an event name and each value is the handler function.

<script>
  import FroalaEditor from "svelte-froala-wysiwyg";
  import 'froala-editor/css/froala_editor.pkgd.min.css';
  import 'froala-editor/css/froala_style.css';

  const config = {
    events: {
      // Fires whenever the content changes (typing, paste, formatting).
      'contentChanged': function () {
        console.log('Content updated!');
        // Inside a handler, `this` is the editor instance,
        // so you can call `this.html.get()` to read the current HTML.
      },
      // Fires when the editor gains focus.
      'focus': function () {
        console.log('Editor focused');
      }
    }
  };
</script>

<FroalaEditor config={config} />

Two useful details:

this is the editor instance. Because these are Froala events, this inside each handler refers to the live editor. That means this.html.get() reads the current content, this.selection.text() reads the selected text, and so on. This only works with a regular function () {}. An arrow function would rebind this and break that access, so use regular functions here.

contentChanged versus bind:model. If all you need is the current HTML in a variable, bind:model already handles it. Reach for contentChanged when you want to react to edits. The full event list is in the events documentation.

Step 5: Initialize on Special Tags with the tag Prop

Froala does not have to live in a div. The tag prop lets you attach the editor to an <img>, <button>, <input>, or <a> element. This is how you get Froala’s inline image or link editing tools around an existing element.

When you use tag, the meaning of model changes. Instead of an HTML string, model becomes an object of the element’s attributes.

<script>
  import FroalaEditor from "svelte-froala-wysiwyg";
  import 'froala-editor/css/froala_editor.pkgd.min.css';
  import 'froala-editor/css/froala_style.css';

  // For a tag editor, model holds the element's attributes,
  // not an HTML string.
  let imageModel = $state({
    src: "https://froala.com/assets/img/logo.png",
    alt: "Froala Logo"
  });
</script>

<!-- tag="img" mounts Froala's image tools on an <img> element. -->
<FroalaEditor tag="img" bind:model={imageModel} />
  • tag names the element type to initialize on. Valid values are img, button, input, and a. Omit it and the editor defaults to a standard rich text div.
  • model in this mode maps to the element’s attributes. Editing the image through Froala updates the bound object, and updating the object in your code updates the element. Read imageModel.src to get the current source after a user swaps or resizes the image.

Step 6: Control Initialization Manually with the manual Prop

By default the editor initializes as soon as the component mounts. Sometimes you want to delay that, for example until data loads or a user clicks a button. Set manual={true} and trigger initialization yourself.

To call the component’s methods, grab a reference to it with Svelte’s bind:this.

<script>
  import FroalaEditor from "svelte-froala-wysiwyg";
  import 'froala-editor/css/froala_editor.pkgd.min.css';
  import 'froala-editor/css/froala_style.css';

  // Holds a reference to the FroalaEditor component instance.
  let editorRef;

  function init() {
    // Kick off initialization on demand.
    editorRef.initializeEditor();
  }
</script>

<!-- In Svelte 5, use onclick (not on:click). -->
<button onclick={init}>Initialize Editor</button>

<FroalaEditor
  bind:this={editorRef}
  manual={true}
  model="<p>Wait for it...</p>"
/>
  • manual set to true stops the editor from auto-initializing. It stays dormant until you call initializeEditor().
  • bind:this={editorRef} stores the component instance in editorRef so you can call its methods. This is standard Svelte and works the same in every version.
  • onclick is the Svelte 5 event syntax. On Svelte 3 or 4, use on:click={init} as the official docs show.

The component exposes three methods for manual control: initializeEditor() to start it, destroy() to tear it down and return the element to its original state, and getEditor() to retrieve the underlying Froala instance.

Step 7: Drive the Editor Programmatically with the editor Prop

bind:model gives you the content as a string. Sometimes you need the whole editor, not just its text: insert HTML at the cursor, read the current selection, toggle formatting, or save an undo step. That is what the editor prop is for.

Binding to editor exposes the live Froala editor instance, so you can drive it with any of Froala’s methods. Instead of receiving the instance through a callback, you bind:editor to pull it into your own variable.

<script>
  import FroalaEditor from "svelte-froala-wysiwyg";
  import 'froala-editor/css/froala_editor.pkgd.min.css';
  import 'froala-editor/css/froala_style.css';

  let htmlContent = $state("<p>Start here.</p>");

  // Holds the live Froala editor instance once it is ready.
  let editorInstance = $state();

  function insertSignature() {
    // Guard against calling methods before the editor exists.
    if (!editorInstance) return;

    // Insert HTML at the current cursor position.
    editorInstance.html.insert('<p>— Sent from my Svelte app</p>');

    // Record an undo step so this insertion can be reverted with Ctrl+Z.
    editorInstance.undo.saveStep();
  }

  function logSelection() {
    if (!editorInstance) return;
    // Read whatever text the user currently has selected.
    console.log('Selected text:', editorInstance.selection.text());
  }
</script>

<button onclick={insertSignature}>Insert Signature</button>
<button onclick={logSelection}>Log Selection</button>

<!--
  bind:model  -> two-way string binding for the content
  bind:editor -> the live instance for calling methods
-->
<FroalaEditor bind:model={htmlContent} bind:editor={editorInstance} />

The mental model that keeps these two props straight:

  • Use bind:model when you want the content. It is a synced HTML string. Reach for it to save, preview, or restore what the user wrote.
  • Use bind:editor when you want to command the editor. It is the full instance and its entire method API. Reach for it to insert at the cursor, read or manipulate the selection, toggle formatting, or trigger undo steps.

The two work together. In the example above, model tracks the content while editor performs the actions.

Conclusion

The official svelte-froala-wysiwyg SDK turns Froala into a first-class Svelte component. You render <FroalaEditor>, bind model for content, pass a config object for everything else, and bind editor when you need to command the editor directly. No lifecycle boilerplate, no manual DOM teardown.

For the complete prop and method reference, see the official Svelte documentation. For the full set of configuration options, browse the options reference, and for everything you can do through the editor instance, see the methods reference.

Ready to build? Install the package, get a license key, drop in the basic example above, and you will have a working editor in under five minutes. The source is on GitHub if you want to dig deeper or file an issue.

FAQ

Do I need to install froala-editor separately?

No. It comes in as a dependency of svelte-froala-wysiwyg. You do need to import its CSS files yourself, since bundlers do not include CSS automatically.

Where do I put my license key?

Inside the config object as key: 'YOUR_KEY'. This is the standard placement across Froala’s framework SDKs.

How do I load Froala plugins like tables or image upload?

Import the plugin’s file from froala-editor/js/plugins/, for example import 'froala-editor/js/plugins/table.min.js';, then add its buttons to toolbarButtons. Import only what you use to keep your bundle small.

What is the difference between bind:model and bind:editor?

bind:model gives you the content as a synced HTML string. bind:editor gives you the live editor instance and its full method API. Use model to read or set content; use editor to command the editor, such as inserting at the cursor or reading the selection.

Does two-way binding work the same in Svelte 5 and older versions?

The bind: directive on the component is the same across versions. What changes is your variable: use $state() in Svelte 5, and a plain let in Svelte 3 or 4.

Froala v5.3.1: Real-Time Collaborative Editing Plugin, Svelte.js Support, and More

Froala 5.3.1 release
Froala Editor v5.3.1 is our biggest release in recent memory, headlined by the brand-new Collaborative plugin that brings real-time editing, comments, suggestions, and version control directly into the editor. This version also adds first-class Svelte.js support, a customizable font-size input, module-friendly Word import, and a reworked clipboard storage mechanism that keeps copied content out of localStorage by default.

Key Takeaways

  • The new Collaborative plugin adds real-time editing, comments, suggestions, and version control.
  • Real-time collaboration and persistence require a backend server (included in the Node SDK).
  • An official Svelte wrapper, svelte-froala-wysiwyg, makes Svelte integration native.
  • Custom font sizes, module-friendly Word import, and more private clipboard handling round out the release.

Heads-Up Before You Upgrade

Good news: v5.3.1 introduces no breaking API changes. Your existing configuration will continue to work as-is. There is one behavioral change worth knowing about before you deploy:

  • Clipboard storage no longer uses localStorage by default. Copied editor content is now written to the Origin Private File System (OPFS) in secure contexts, with an automatic localStorage fallback for non-secure contexts. This change happens automatically and requires no configuration. See the Improvements section for details.

What’s New in v5.3.1

New Features

  • Collaborative plugin. Real-time editing, inline comments, tracked-change suggestions, and version control, all in one plugin. This is the flagship feature of the release, covered in detail below.
  • Svelte.js support. An official Svelte wrapper, svelte-froala-wysiwyg, lets you drop the editor into a Svelte project with two-way data binding and full access to Froala’s options and events, no custom integration required.
  • Custom font sizes. The font-size dropdown now includes a custom option that opens a popup for entering an exact size, with configurable minimum and maximum limits so users can set the precise size they need without leaving the toolbar.

Improvements

  • Configurable Mammoth instance for Import from Word. A new importFromWordMammoth option lets you pass the Mammoth library as a module, which makes the Import from Word plugin work cleanly in modern build tools like Vite, Webpack, and other ES-module bundlers instead of relying on a global window.mammoth.
  • More private clipboard handling. Copied content is no longer persisted to localStorage by default. Froala now uses the Origin Private File System (OPFS) for clipboard metadata in secure contexts, reducing exposure of potentially sensitive content. See Private Clipboard Handling below for details.

Deep Dive: The Collaborative Plugin

Editing content is rarely a solo activity. Teams co-author articles, review contracts, and approve knowledge-base updates together, and until now that meant leaving the editor to trade changes over email, exports, and Word round-trips. Teams looking to boost remote team collaboration can also use shared visual workspaces such as online whiteboards to brainstorm ideas, exchange feedback, organize content, and keep distributed participants engaged. The result was overwritten content from simultaneous edits, slow approval cycles, and no reliable history to roll back to.

The new Collaborative plugin solves this by bringing four highly requested capabilities into the editor itself:

  • Real-time editing. Every editor instance stays in sync as users type. A conflict-free replicated data type (CRDT) engine resolves concurrent edits and presence indicators show active cursors, so two people can edit the same document without overwriting each other.

Real-time editing

  • Comments. Highlight any text and attach a threaded, inline comment. Collaborators can reply, resolve threads, and @mention teammates, all anchored to the exact content under discussion.

comments

  • Suggestions. Reviewers propose insertions, deletions, and rewrites without touching the live document. Owners accept or reject each change individually.

Suggestions

  • Version control. Browse timestamped snapshots with author attribution, compare any two versions side by side, and restore a prior state in one click.

Version control

How It Works

The Collaborative plugin operates in two modes. In real-time mode, editors that share the same docId connect to a WebSocket server and collaborate live. In asynchronous (offline-first) mode, the plugin instead reads and writes collaborative data through REST endpoints. The single switch that flips between them is collabConfig.realTime.syncUrl: set it to enable real-time collaboration, or leave it null to fall back to async mode.

Comments, suggestions, and version history are shared live between connected users, but that live data is not persisted on its own. To store it in a database, point the editor at your backend using commentsUrl, suggestionsUrl, and versionControl.url.

Configuring the Editor

Here is a real-time setup with a user identity, a shared document ID, persistence URLs for comments, suggestions, and versions, and periodic auto-saved snapshots:

new FroalaEditor('.selector', {
  collabConfig: {
    user: {
      id: 'user-42',
      name: 'Alice',
      role: 'editor' // 'editor' | 'suggester' | 'viewer'
    },
    docId: 'my-doc',
    commentsUrl: 'http://localhost:3000/collab/my-doc/comments',
    suggestionsUrl: 'http://localhost:3000/collab/my-doc/suggestions',
    realTime: {
      syncUrl: 'ws://localhost:3000',
      reconnectDelay: 2000
    },
    versionControl: {
      url: 'http://localhost:3000/collab/my-doc/versions',
      autoSaveEnabled: true,
      autoSaveInterval: 300000 // save a snapshot every 5 minutes
    }
  }
});

The plugin also exposes methods and events for driving collaboration from your own UI. For example, you can switch modes, add a comment on the current selection, or save a named version snapshot:

var editor = new FroalaEditor('.selector', {}, function () {
  // Switch the active collaboration mode (respects the user's role).
  editor.collaborative.setMode('suggesting');

  // Add a comment anchored to the currently selected text.
  editor.collaborative.addComment('Please clarify this sentence.');

  // Save a named version snapshot.
  editor.collaborative.saveVersion('Draft v1', 'Before major revisions')
    .then(function (record) {
      console.log('Saved version:', record.id);
    });
});

And you can react to collaboration activity through events such as collab.connectionStatus, collab.remoteContentChanged, and version:create:

new FroalaEditor('.selector', {
  events: {
    'collab.connectionStatus': function (status) {
      // 'connecting' | 'connected' | 'disconnected'
      console.log('Connection status:', status);
    },
    'collab.remoteContentChanged': function () {
      console.log('Remote content updated.');
    },
    'version:create': function (record) {
      console.log('New version saved:', record.title, 'at', record.createdAt);
    }
  }
});

Setting Up the Backend

Real-time collaboration and persistence require a backend server. The Collaborative backend ships in the wysiwyg-editor-node-sdk package and is split into four modules: a WebSocket relay for real-time peer sync, REST endpoints for comments and suggestions, a version history store, and an async content-save endpoint. All four can share a single HTTP server and a single SQLite file, and the required better-sqlite3 dependency installs automatically with the package.

A minimal Express server that runs the WebSocket relay and all REST routes on one port looks like this:

const http = require('http');
const path = require('path');
const express = require('express');

const FroalaEditor = require('wysiwyg-editor-node-sdk');
const Collaborative = FroalaEditor.Collaborative;
const CollabPersistence = FroalaEditor.CollabPersistence;
const VersionControl = FroalaEditor.VersionControl;
const AsyncSave = FroalaEditor.AsyncSave;

const app = express();
app.use(express.json());

// REST routes for comments + suggestions, version history, and async save.
// All three can share a single SQLite file.
const dbPath = path.join(__dirname, 'data', 'collab.db');
CollabPersistence.attachRoutes(app, { dbPath: dbPath });
VersionControl.attachRoutes(app, { dbPath: dbPath });
AsyncSave.attachRoutes(app, { dbPath: dbPath });

// Share a single HTTP server between Express and the WebSocket relay.
const server = http.createServer(app);
Collaborative.attachToServer(server);

server.listen(3000, function () {
  console.log('Collaborative backend listening on port 3000');
});

Once the server is running, clients that connect with the same docId are treated as peers: the relay broadcasts every sync message to the other members of that document, while the REST endpoints (namespaced under /collab/:docId/) persist comments, suggestions, and version snapshots. For the full endpoint reference, request and response schemas, and configuration options, see the docs.

More in This Release

Custom Font Sizes

Users sometimes need a size that isn’t in the preset list. Adding custom to the fontSize array surfaces a Custom option in the dropdown that opens a popup for entering an exact value, so users can hit the precise size they want without a workaround. Two new options, fontSizeCustomMin (default 8) and fontSizeCustomMax (default 96), validate the entered value. Existing preset sizes behave exactly as before. Note that if you override the default fontSize array and still want this feature, you must explicitly include 'custom'.

new FroalaEditor('.selector', {
  // Add 'custom' to expose the manual-entry option.
  fontSize: ['8', '12', '18', '24', '36', '48', 'custom'],
  fontSizeCustomMin: 8,
  fontSizeCustomMax: 96
});

Svelte.js Support

Svelte developers now have an official Svelte wrapper for Froala Editor. The svelte-froala-wysiwyg component gives you a native Svelte way to drop the editor into a project, so you no longer have to build or maintain your own integration to get the editor running in a Svelte app. It ships with two-way data binding, full access to Froala’s options and events, support for initializing on special tags, and manual control over the editor lifecycle.

To get started, run:

npm install svelte-froala-wysiwyg

Configurable Mammoth for Import from Word

The Import from Word plugin previously expected mammoth.js on the global window object, which is awkward in module-based builds. The new importFromWordMammoth option lets you pass the library instance explicitly, so the plugin works in Vite, Webpack, and ES-module setups. It defaults to window.mammoth, and you can set it to false to disable it. The Mammoth library must still be available before the editor initializes.

import mammoth from 'mammoth';

new FroalaEditor('.selector', {
  importFromWordMammoth: mammoth
});

Import from Word plugin docs

Private Clipboard Handling

Previously, content copied within the editor could be persisted in the browser’s localStorage, which meant user-generated content, potentially including sensitive or personally identifiable information, could linger beyond the editing session. That raised data-minimization and storage-limitation concerns under regulations such as GDPR, with no way to control the behavior.

In v5.3.1, Froala no longer persists clipboard content to localStorage by default. Clipboard metadata is now stored in the Origin Private File System (OPFS), a more isolated, browser-managed storage mechanism that reduces exposure through standard browser storage APIs. Because OPFS is only available in secure contexts, the editor checks window.isSecureContext and automatically falls back to localStorage on non-secure domains, so copy and paste keep working reliably everywhere. Native browser copy/paste behavior and compatibility with external applications are fully preserved, and the change requires no configuration on your part.

Much More…

This release includes additional improvements and bug fixes. Including:

  • Improved: Enhanced the Dark theme with refined styling for popups to improve visibility and consistency.
  • Fixed: Browser spellcheck no longer expands the selection beyond the target word into adjacent text nodes (such as after <br> elements), preventing unintended text deletion.
  • Fixed: Word counter now correctly blocks input at the maximum limit even when followed by punctuation.

See the complete changelog for details.

Upgrade to v5.3.1 Today

Froala Editor v5.3.1 is a major step forward in collaboration, developer experience, and privacy. With a full real-time Collaborative plugin, native Svelte.js support, customizable font sizes, module-friendly Word import, and more private clipboard handling, this release delivers tangible value for developers and end users alike.

Key Benefits

  • Collaborate in real time. Real-time editing, comments, suggestions, and version control let teams co-author in the editor instead of trading exports and email threads, with no overwrites from simultaneous edits.
  • Drop into modern stacks. Native Svelte.js support and a module-friendly Import from Word option mean Froala fits cleanly into Svelte, Vite, Webpack, and ES-module builds.
  • Give users precise control. The custom font-size input lets users enter an exact size, with configurable min and max limits, without leaving the toolbar.
  • Keep content more private. Clipboard data stays out of localStorage by default, reducing exposure of potentially sensitive content while copy and paste keep working reliably everywhere.
  • Upgrade with confidence. No breaking API changes, so your existing configuration keeps working as-is.

Whether you are building the next generation of content management systems, developing collaborative enterprise applications, or simply want the most reliable and feature-rich editing experience available, v5.3.1 has something for you.

Upgrade to the latest version and start collaborating in real time.

Ready to Upgrade?

Check our getting-started guide to know how to download the latest Froala Editor release and how to include it in your project based on your preferred method. As always, our support team is here to help if you have any questions or need assistance with the upgrade process.

Upgrade to Froala Editor v5.3.1 today and unlock new possibilities.

Try the Latest Froala Editor

Explore a variety of examples that demonstrate the functionality of the Froala HTML Editor.

Thank you for being part of the Froala community. We are committed to continuously improving and delivering the tools you need to build exceptional content experiences.

Froala vs AIEditor: Which Rich Text Editor Should You Choose in 2026?

Froala vs AIEditor: Which Rich Text Editor Should You Choose in 2026?

If you’re adding a rich text editor to your app, you’ve probably come across AIEditor and Froala.

AIEditor is a newer, open-source editor built with AI in mind. Froala has been around for over a decade and is used in CMS platforms, CRMs, email builders, and other applications.

So, which one should you choose?

There isn’t one answer that works for every project. In this article, we’ll compare AIEditor and Froala based on pricing, built-in features, AI support, and what it’s actually like to use them as your app grows.

Let’s get into it.

Key Takeaways

  • AIEditor is an open-source, AI-focused editor with a generous free version, but its Pro version costs $5,000–$8,000.
  • Froala starts at around $799/year and includes unlimited editor loads, unlimited users, built-in plugins, and technical support.
  • Both editors support AI, but Froala’s AI Assist works with multiple AI providers and gives you more control over AI-generated content.
  • AIEditor works well for small projects, while Froala is a better fit for products that need to scale and be maintained long-term.
  • Both editors are quick and easy to integrate.

Quick Comparison Table

Before we get into the setup and code, here’s a quick side-by-side comparison of AIEditor and Froala.

This table gives you a quick look at their pricing, features, AI support, plugins, and more. We’ll cover each of these points in more detail later in the article.

Feature AIEditor Froala
License model LGPL (open source) + paid Pro tiers Commercial subscription/perpetual license
Starting price Free (open source) / $5,000+ for Pro ~$799/year (Professional plan)
Users & editor loads Yes, on both open source and Pro Yes, on every paid plan
Framework support React, Vue, Angular, Svelte, and more React, Angular, Vue with maintained LTS SDKs
Plugin ecosystem Built-in features Full plugin library included on all plans
File management Manual setup Native Filestack integration (uploads, CDN, image editing)
AI features AI writing, translation, chat, and code tools AI Assist plugin, works with any LLM provider
Support Community + direct contact for Pro Dedicated technical support
Best for Small projects and developers who prefer open source Teams shipping production apps that need predictable cost and support

 

At first glance, AIEditor and Froala look quite similar. Both are capable rich text editors with AI features and modern framework support.

The biggest difference is pricing. AIEditor Pro uses a one-time license fee, while Froala offers annual and perpetual license options. So, it’s worth looking at the long-term cost rather than just the starting price. Identifying these pricing structures is a vital step toward selecting the best WYSIWYG editors for developers who need to reconcile immediate budget constraints with long-term scalability.

A visual diagram showing 3 year cost comparison of Froala and AI editor

With that in mind, let’s see how easy each editor is to set up and use.

Setting Up AIEditor

AIEditor is easy to add to a simple HTML page. You don’t need npm or any build tools to get started.

<!DOCTYPE html>

<html>

  <head>

    <link rel="stylesheet" href="<https://cdn.jsdelivr.net/npm/aieditor/dist/style.css>" />

  </head>

  <body>

    <div id="editor-container" style="height: 400px;"></div>

    <script type="module">

      import { AiEditor } from "<https://esm.run/aieditor>";

      const editor = new AiEditor({

        element: "#editor-container",

        placeholder: "Start typing here...",

      });

    </script>

  </body>

</html>

That’s it. AIEditor gives you a Notion-style block editor with drag-and-drop, Markdown support, and basic formatting, all loaded directly from a CDN.

You can find the full option list and more advanced examples in the AIEditor GitHub repository.

A visual diagram showing AI editor interface

AI features require some additional setup. You’ll need to connect an AI model and configure how requests are handled.

Froala takes a similar approach. Its AI Assist plugin provides the AI interface and built-in actions for generating content, changing tone, and translating text. You still need to connect an AI endpoint or handle requests yourself, but you don’t have to build the AI editing interface from scratch.

Setting Up Froala

Now, let’s try the same with Froala. The setup is just as quick, and the Froala documentation includes examples for almost every common use case.

Like AIEditor, Froala can also be loaded directly from a CDN, so you only need a simple HTML page to get started:

<!doctype html>

<html>

  <head>

    <link

      href="<https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css>"

      rel="stylesheet"

      type="text/css"

    />

  </head>

  <body>

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

    <script

      type="text/javascript"

      src="<https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js>"

    ></script>

    <script>

      new FroalaEditor("#editor-container", {

        placeholderText: "Start typing here...",

        toolbarButtons: [

          "bold",

          "italic",

          "underline",

          "strikeThrough",

          "fontFamily",

          "fontSize",

          "textColor",

          "backgroundColor",

          "alignLeft",

          "alignCenter",

          "alignRight",

          "formatOL",

          "formatUL",

          "insertLink",

          "insertImage",

          "insertTable",

          "insertHR",

          "undo",

          "redo",

          "html",

        ],

      });

    </script>

  </body>

</html>

A visual diagram showing froala WYSIWYG editor interface

Adding AI Assist

Froala’s AI Assist plugin adds AI tools directly to the editor. To enable it, load the AI Assist plugin, accept the AI supplemental terms, and add the AI buttons to the toolbar:

<!doctype html>

<html>

  <head>

    <link

      href="<https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css>"

      rel="stylesheet"

      type="text/css"

    />

  </head>

  <body>

    <div

      id="editor-container"

    ></div>

    <script src="<https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js>"></script>

    <script src="<https://cdn.jsdelivr.net/npm/froala-editor@latest/js/plugins/ai_assist.min.js>"></script>

    <script>

      new FroalaEditor("#editor-container", {

        placeholderText: "Start typing here...",

        aiSupplementalTermsAccepted: true,

        toolbarButtons: [

          "bold",

          "italic",

          "underline",

          "strikeThrough",

          "fontFamily",

          "fontSize",

          "textColor",

          "backgroundColor",

          "alignLeft",

          "alignCenter",

          "alignRight",

          "formatOL",

          "formatUL",

          "insertLink",

          "insertImage",

          "insertTable",

          "aiAssist",

          "aiShortCuts",

          "undo",

          "redo",

          "html",

        ],

      });

    </script>

  </body>

</html>

That’s it. The AI Assist options now appear in the editor toolbar, so users can access AI tools directly inside the editor without needing a separate AI interface.

A visual diagram showing AI assist plugin interface in Froala WYSIWYG editor

But Froala doesn’t limit you to a specific AI provider. You can use the aiAssistRequest option to control how AI requests are sent and connect the editor to the AI provider of your choice.

Here’s a simple example:

new FroalaEditor('#editor-container', {

  aiAssistRequest: async function(data, signal) {

    const response = await fetch('<https://my-ai-api.com/generate>', {

      method: 'POST',

      headers: { 'Authorization': 'Bearer TOKEN' },

      body: JSON.stringify({

        prompt: data.prompt,

        max_tokens: 500

      }),

      signal: signal

    });

    const result = await response.json();

    return result.generated_text;

  }

});

In this example, aiAssistRequest sends the prompt to the OpenAI API and returns the generated content to Froala. The editor then adds the AI response to the editing workflow.

This direct setup is useful for testing and prototypes. For production apps, keep your AI API key on the server and send requests through your own backend instead.

The setup is simple, but there are a few things you should keep in mind when adding a rich text editor to a real application.

Best Practices

Here are a few best practices to follow when working with AIEditor or Froala:

  • Keep your AI API keys on the server. Never expose your OpenAI, Claude, or Gemini API key in frontend code. Use a backend endpoint or serverless function to handle AI requests.
  • Add only the toolbar buttons you need. Both editors let you choose which buttons appear in the toolbar. Keeping the toolbar simple makes the editor easier to use.
  • Test content pasted from Word or Google Docs. Pasted content can include extra HTML and inline styles, so test how the editor handles it before launching your app.
  • Set content limits. Use word or character limits to prevent users from adding more content than your application or database expects.
  • Check the license before going to production. Make sure you understand what the editor’s license allows before using it in a production application.

Following those practices will save you from most headaches, but a few mistakes are common enough and costly enough that they deserve a closer look on their own.

Common Pitfalls

Even with a simple setup, there are a few common mistakes you should avoid:

  • Assuming open source means every feature is free. AIEditor’s core editor is open source, but some advanced features are only available in the Pro version. Check which features you need before choosing a license.
  • Ignoring long-term support. Adding a rich text editor is easy, but you may run into paste issues, mobile bugs, or other edge cases later. Consider what kind of technical support you’ll need as your app grows.
  • Exposing AI API keys in frontend code. If you connect an AI provider to your editor, never add the API key directly to your frontend code. Keep it on your server and handle AI requests through your backend.
  • Not checking framework support early. If your app may move to a different framework later, check how well the editor supports and maintains its framework integrations.

Those common mistakes also show why choosing a rich text editor isn’t just about how quickly you can set it up. You also need to think about pricing, support, integrations, and how well the editor will work as your application grows. This complexity is particularly evident when analyzing how Froala compares to Eddyter regarding the balance between feature-rich toolsets and long-term budget management for growing applications.

Why Teams Lean Toward Froala for Production Apps

Both AIEditor and Froala are capable editors. AIEditor offers a lot in its free, open-source version. But for production applications, Froala has a few practical advantages: When considering how these veteran platforms stack up against other enterprise-ready options, our analysis of <a Froala vs CKEditor in 2026 provides essential context for your technical roadmap.

  • More predictable pricing. Froala’s paid plans start at $799/year, while AIEditor Pro costs $5,000–$8,000 as a one-time license. For teams that don’t want a large upfront cost, Froala can be easier to budget for.
  • A built-in plugin library. Froala includes plugins for tables, images, file uploads, and other common editing features. It also integrates with Filestack for advanced file and image management.
  • Flexible AI integration. With aiAssistRequest, you control how AI requests are handled. This makes it possible to connect different AI providers or your own AI service.
  • Maintained framework SDKs. Froala provides SDKs for React, Angular, and Vue, making it easier to use the editor in modern web applications.
  • Technical support. Froala provides technical support for paid users, which can be helpful when you run into integration or editor issues in production. Independent reviews on G2 echo this, with users regularly citing responsive support as a standout.

So, which editor should you choose?

Conclusion

If you’re building a small project or want a free, open-source editor, AIEditor is a good option. It’s easy to set up and offers useful AI-focused features.

But if you’re building a production application that you plan to maintain and scale, Froala may be a better fit. Its plugin library, flexible AI integration, maintained framework SDKs, and technical support can make long-term development easier.

Both editors are capable, so the best choice depends on your project and budget. Try them in your own application and see which one works better with your stack and requirements.

Ready to build with a production-ready rich text editor? Explore Froala’s AI Assist, plugin library, framework SDKs, and file management features to see how quickly you can integrate a flexible editor into your app.

Froala vs Eddyter: Which Rich Text Editor Is Right for Your App in 2026?

Froala vs Eddyter: Which Rich Text Editor Is Right for Your App in 2026?

If you’re adding a rich text editor to your app, you may have come across Eddyter and Froala.

Eddyter is a newer, AI-focused editor built for React and Next.js applications. It handles features like AI and storage as part of its managed setup. Froala, on the other hand, is an established WYSIWYG editor used in CMS platforms, CRMs, email builders, and other applications.

Both editors help you add rich text editing to your app, but they take different approaches. In this article, we’ll compare their features, pricing, AI support, and setup to help you choose the right one for your project.

Key Takeaways

  • Eddyter is a managed editor built on top of Lexical (Meta’s open-source editor framework) for React and Next.js apps. It requires an account and API key to use, even on the free tier.
  • Froala works with React, Angular, Vue, and plain JavaScript, giving you more flexibility across different projects.
  • Eddyter uses tiered subscription pricing, while Froala offers annual and perpetual licensing.
  • Both offer real AI features, but Eddyter bundles AI, storage, and hosting into one subscription, while Froala’s AI Assist plugs into whichever AI provider you choose, and you host everything yourself.
  • Eddyter is a good fit for React and Next.js apps that need a managed setup. Froala is better suited for teams that need more framework flexibility and control.

Quick Comparison Table

Here’s a side-by-side look at how the two editors differ before we get into setup and code.

Feature Eddyter Froala
License model Managed SaaS subscription Commercial subscription/perpetual license
Starting price ~$590/year (AI pro managed plan) ~$799/year (Professional plan)
Framework support React and Next.js only React, Angular, Vue, or plain HTML/JS
Hosting model Managed platform with storage and AI infrastructure Self-hosted (you control your own backend)
Account/API key required Yes, on every plan No account needed for the core editor
Usage limits Limits vary by plan Unlimited editor loads and users
AI features AI chat, autocomplete, and tone tools AI Assist plugin, works with any LLM provider
Editor architecture Built on Lexical Built and maintained by Froala
Support Email support Dedicated technical support
Best for Teams that want a managed editor setup Teams that need framework flexibility and predictable costs at scale

The biggest difference between Eddyter and Froala is how they are set up.

Eddyter is a managed platform that includes storage and AI infrastructure. This can make setup easier, but limits such as editor loads, storage, and AI credits depend on your plan.

Froala runs directly in your application and gives you more control over your editor setup. It also includes unlimited editor loads and users, so your editor usage doesn’t increase the license cost as your application grows. Not every vendor prices that way, and the top WYSIWYG editors to consider in 2026 splits them by exactly this: what happens to the bill as you grow.

The pricing difference becomes easier to understand when you compare both editors over a few years:

A visual diagram showing 3 year cost comparison of Froala and Eddyter

With that difference in mind, let’s look at what setup actually looks like for each.

Setting Up Eddyter

Eddyter is built specifically for React and Next.js, so setup goes through npm rather than a CDN. You’ll also need an API key from an Eddyter account before the editor will render, even on the free tier.

npm install eddyter

Then, add the editor to your application:

"use client";

import { ConfigurableEditorWithAuth, EditorProvider } from "eddyter";

import "eddyter/style.css";

export default function MyEditor() {

  const apiKey = process.env.NEXT_PUBLIC_EDDYTER_API_KEY;

  return (

    <EditorProvider>

      <ConfigurableEditorWithAuth

        apiKey={apiKey}

        onChange={(html) => console.log("Editor content:", html)}

      />

    </EditorProvider>

  );

}

Add your Eddyter API key to the environment file:

# .env.local

NEXT_PUBLIC_EDDYTER_API_KEY=your_api_key_here

That’s it. Once the editor is set up, you can use features like AI chat, tables, embeds, and slash commands directly in your app.

The setup is quick for React and Next.js projects. Since the editor uses Eddyter’s API key and EditorProvider, you’ll also need an Eddyter account to get started.

You can find the full setup walkthrough in Eddyter’s own quick-install guide.

Setting Up Froala

Froala takes a different approach. You don’t need an API key or a specific framework to set up the editor. It can be loaded directly from a CDN, so a simple HTML page is enough to get started:

<!DOCTYPE html>

<html>

  <head>

    <link

      href="<https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css>"

      rel="stylesheet"

      type="text/css"

    />

  </head>

  <body>

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

    <script

      type="text/javascript"

      src="<https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js>"

    ></script>

    <script>

      new FroalaEditor("#editor-container", {

        placeholderText: "Start typing here...",

        toolbarButtons: ["bold", "italic", "underline", "insertImage", "insertLink"],

      });

    </script>

  </body>

</html>

That’s it. The editor is now ready to use in your application. You can also add more toolbar buttons and plugins based on the features your app needs.

A visual diagram showing the interface of froala WYSIWYG editor

Adding AI Assist

Froala also lets you add AI features using its AI Assist plugin. With aiAssistRequest, you can control how AI requests are handled and connect the editor to your own backend:

<script>

  new FroalaEditor("#editor-container", {

    toolbarButtons: ["bold", "italic", "aiAssist"],

    aiSupplementalTermsAccepted: true,

    aiAssistRequest: async function (data, signal) {

      const response = await fetch("/api/ai-assist", {

        method: "POST",

        headers: { "Content-Type": "application/json" },

        body: JSON.stringify({ prompt: data.prompt }),

        signal: signal,

      });

      const result = await response.json();

      return result.html;

    },

  });

</script>

In this example, aiAssistRequest sends the AI prompt to your own backend endpoint. Your backend can then connect to OpenAI, Claude, Gemini, or another AI model.

A visual showing the ai assist plugin interface of froala WYSIWYG editor

This is different from Eddyter’s managed setup. Eddyter connects the editor to its platform using an API key, while Froala gives you control over how and where AI requests are handled.

Best Practices

Before using either editor in a production application, here are a few things to keep in mind:

  • Keep AI API keys on the server. If you’re connecting an AI provider to Froala using aiAssistRequest, never expose the API key in your frontend code. Handle AI requests through your backend instead.
  • Check framework support early. Make sure the editor works with your current framework and any frameworks your team may use in the future.
  • Think about long-term costs. Don’t compare pricing based only on your current usage. Consider how storage, AI usage, and other limits may change as your application grows.
  • Test content pasted from Word or Google Docs. Pasted content can include unnecessary HTML and styles, so test how the editor handles it before launch.
  • Check how your data is handled. If you’re using a managed editor platform, understand where your content is stored and how it moves through the service before using it in production.

Common Pitfalls

Even with a simple setup, there are a few common mistakes to avoid:

  • Ignoring usage limits. Eddyter plans have different storage and AI usage limits. As your application grows, you may need to move to a higher plan.
  • Not thinking about platform dependency. Eddyter uses a managed platform for features such as storage and AI. This means parts of your editor setup depend on Eddyter’s services. Froala gives you more control over how these services are handled.
  • Skipping a framework check. Always check whether the editor supports the frameworks your application uses before choosing it.
  • Exposing AI API keys. If you use Froala’s aiAssistRequest or another custom AI integration, keep your AI credentials on the server and send requests through your backend.

The differences above become more important when you’re building an editor for a production application. Setup is only one part of the decision. You also need to think about framework support, costs, and long-term maintenance.

Why Teams Lean Toward Froala for Production Apps

Eddyter makes it easy to add an AI-powered editor to your app, especially if you want storage and AI infrastructure handled for you. But for teams building and scaling production applications, Froala has a few advantages: While Eddyter addresses modern AI needs, developers also need to weigh these enterprise features against how Froala compares to CKEditor regarding long-term support and functional parity.

  • More framework flexibility. Froala works with React, Angular, Vue, and plain JavaScript, making it easier to use across different applications and tech stacks.
  • Predictable costs as usage grows. Froala includes unlimited editor loads and users, while Eddyter plans have different storage and AI usage limits.
  • More control over your editor setup. Froala’s core editor runs directly in your application and doesn’t require an external API key to initialise.
  • A mature editor. Froala has been used in production applications for years and is built for use cases such as CMS platforms, CRMs, and email builders.
  • Technical support. Froala’s paid plans include technical support, which can be useful when you run into integration or production issues.

So, which editor should you choose?

Conclusion

If you want a managed editor with AI and storage included, Eddyter is worth trying. It’s quick to set up and can reduce the amount of infrastructure you need to manage.

If you need more framework flexibility and control over your editor setup, Froala may be a better fit. It works across different frameworks and lets you choose how features like AI and file management are handled. Developers who prioritize advanced automation might find additional clarity on feature parity through our comparison of Froala vs AIEditor in 2026.

Both editors take different approaches, so the right choice depends on your application’s requirements, budget, and tech stack. Try both in a small project before choosing one for your production application.

Ready to build with a flexible rich text editor? Explore Froala’s framework SDKs, AI Assist, plugins, and developer documentation to see how quickly you can add a production-ready editor to your app.

How to Build a Slash Command Quick Action Menu in Froala Editor

Slash Command Quick Action Menu

Notion does it. Linear does it. GitHub does it. Users now expect to type / and get an instant menu of content options. If you are integrating Froala V5 into your app, you can build the same experience from scratch using Froala’s keyup event, its html.insert() method, and a custom-rendered dropdown. No third-party library required.

In this tutorial, you’ll learn how to add a fully functional slash command menu to Froala Editor. We’ll build it from scratch, explaining every decision so you understand not just what the code does, but why.

By the end, you’ll have a clean, keyboard-friendly command menu that works reliably with Froala’s contenteditable surface.

Slash Command Quick Action Menu

Key Takeaways

  • Froala V5 does not ship a native slash command menu, but its event and methods API gives you everything you need to build one.
  • You intercept the / keystroke using the keyup event, then render and position a custom dropdown relative to the cursor.
  • Each menu item calls editor.html.insert() or a native Froala command to inject content at the correct position.
  • Proper cleanup (removing the / trigger character, dismissing the menu on Escape or blur) is critical for a polished UX.

Prerequisites

To follow along, you need:

  • Basic knowledge of HTML, CSS, and JavaScript
  • Familiarity with Froala Editor (initialization and events)
  • A Froala license key (or use the trial)

We’ll use the official CDN for simplicity.

The Complete Approach

The implementation has three main responsibilities:

  1. Detect when the user types / at the start of a line
  2. Render a filtered, keyboard-navigable menu
  3. Execute the chosen action and clean up the trigger character

See the full, working implementation on JSFiddle. You can read the complete code with inline comments explaining every non-obvious decision, and test it live in your browser by typing / in the editor.

Let’s break down the code section by section.

1. HTML Structure

<div id="editor-container">
<div id="froala-editor">    <p>Type <strong>/</strong> at the start of a new line to open the command menu.</p>  </div>
</div>
<div id="slash-menu"></div>

The editor lives in its own container for easy styling. The #slash-menu element is placed outside the editor so it can be positioned absolutely relative to the viewport.

2. Styling the Menu

The CSS creates a clean, Notion-style dropdown:

  • Fixed width (260px) with max-height and scrolling
  • Category headers in uppercase
  • Icon + title + description layout
  • Hover and .is-active states for keyboard navigation
  • Subtle shadows and borders for depth

These styles are intentionally decoupled from Froala’s theme so you can easily adapt them.

/* ─── Slash Command Menu Styles ─────────────────────────────── */
    #slash-menu {
      position: absolute;
      z-index: 9999;
      background: #ffffff;
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
      width: 260px;
      max-height: 320px;
      overflow-y: auto;
      display: none; /* Hidden by default */
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }

    #slash-menu .slash-menu-header {
      padding: 8px 12px 4px;
      font-size: 11px;
      font-weight: 600;
      color: #94a3b8;
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    #slash-menu .slash-menu-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 12px;
      cursor: pointer;
      transition: background 0.1s ease;
    }

    #slash-menu .slash-menu-item:hover,
    #slash-menu .slash-menu-item.is-active {
      background: #f1f5f9;
    }

    #slash-menu .slash-menu-item .item-icon {
      width: 32px;
      height: 32px;
      border-radius: 6px;
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 15px;
      flex-shrink: 0;
    }

    #slash-menu .slash-menu-item .item-text .item-title {
      font-size: 13px;
      font-weight: 500;
      color: #1e293b;
      line-height: 1.3;
    }

    #slash-menu .slash-menu-item .item-text .item-description {
      font-size: 11px;
      color: #94a3b8;
      line-height: 1.3;
    }

    /* Editor wrapper */
    #editor-container {
      max-width: 800px;
      margin: 40px auto;
      padding: 0 20px;
    }

3. Defining the Command Data

const SLASH_COMMANDS = [
  {
    category: 'Text',
    items: [
      { id: 'paragraph', title: 'Text', description: 'Plain paragraph', icon: '¶', action: ... },
      ...
    ]
  },
  ...
];

This data structure is the heart of the feature. Each item contains:

  • id: Used for potential future features (like analytics)
  • title and description: Displayed in the UI
  • icon: Simple visual indicator
  • action: A function that receives the editor instance

Why separate actions from the UI? It keeps the data declarative. You can later load commands from an API or allow users to customize them without touching the rendering logic.

4. State Management

let menuVisible = false;
let activeIndex = 0;
let flatItems = [];
let editorInstance = null;

We maintain minimal state:

  • menuVisible controls visibility logic
  • activeIndex tracks which item is highlighted for keyboard navigation
  • flatItems holds the currently visible (filtered) items with their DOM references
  • editorInstance gives us access to Froala methods inside event handlers

5. Building the Menu DOM (buildMenu)

This function is called every time the user types after a /.

function buildMenu(filter) {
  menuEl.innerHTML = '';
  flatItems = [];

  const term = (filter || '').toLowerCase();

  SLASH_COMMANDS.forEach(function(group) {
    const filtered = group.items.filter(item =>
      item.title.toLowerCase().includes(term) ||
      item.description.toLowerCase().includes(term)
    );

    if (filtered.length === 0) return;

    // Create category header
    // Create item rows with click handlers
    // Push to flatItems
  });
}

Key details explained:

  • We flatten the data while respecting categories
  • Filtering happens on both title and description
  • We use mousedown instead of click on menu items. This is critical because click fires after blur, which would close the menu before the selection logic runs.

6. Positioning the Menu

function positionMenu() {
  const range = window.getSelection().getRangeAt(0).cloneRange();
  range.collapse(true);
  const rect = range.getBoundingClientRect();
  // Calculate top and left using scroll offsets
}

We use getBoundingClientRect() on a collapsed range to get the cursor’s exact position. This is more reliable than trying to measure the editor’s internal elements.

We also add a small horizontal boundary check so the menu doesn’t overflow the viewport.

7. Detecting the Slash Trigger (getSlashTriggerText)

This is the most important logic function:

function getSlashTriggerText() {
  const text = node.textContent.slice(0, range.startOffset);
  const slashIndex = text.lastIndexOf('/');
  
  if (slashIndex === -1) return null;
  
  const beforeSlash = text.slice(0, slashIndex).trim();
  if (beforeSlash.length > 0) return null;
  
  return text.slice(slashIndex + 1);
}

Why this logic matters:

  • We only trigger when / is the first non-whitespace character on the current line
  • We extract everything after / as the filter term
  • This prevents the menu from appearing when someone types “hello/world”

8. Cleaning Up the Trigger Character (selectItem)

When the user selects a command, we must remove the / (and any filter text) before inserting the new content:

const text = node.textContent;
const slashIndex = text.lastIndexOf('/');
node.textContent = text.slice(0, slashIndex) + text.slice(range.startOffset);

We then recreate the range and place the cursor back where the / was. This is delicate work because we’re manually editing a text node inside a contenteditable area.

After cleanup, we call the item’s action function and restore focus to the editor.

9. Froala Event Integration

The real power comes from hooking into Froala’s events:

events: {
  'keyup': function(e) { ... },
  'keydown': function(e) { ... },
  'blur': function() { ... }
}

keyup handler responsibilities:

  • Ignore modifier keys
  • Handle Escape to close the menu
  • Manage arrow keys and Enter when the menu is visible
  • Check for the slash trigger on every keystroke

keydown handler:

  • Prevents default behavior for Enter and arrow keys when the menu is open
  • This stops Froala from inserting a new paragraph when the user is just navigating the menu

blur handler:

  • Uses a small setTimeout (150ms) so that mousedown events on menu items have time to fire first

10. Keyboard Navigation

We maintain a flat list of visible items. Arrow keys simply increment/decrement activeIndex and call highlightItem().

highlightItem does two things:

  1. Toggles the is-active class
  2. Calls scrollIntoView({ block: 'nearest' }) so long menus scroll automatically

This creates a smooth experience similar to native dropdowns.

Full Flow Summary

  1. User types / at the start of a line
  2. keyup detects the trigger via getSlashTriggerText()
  3. showMenu() builds filtered DOM and positions it
  4. User can type to filter, use arrows to navigate, or click
  5. On selection, we remove the / text, execute the action, hide the menu, and refocus

Extending the Menu

Adding new content types requires only adding a new object to SLASH_COMMANDS. Here is a file upload entry as an example:

{
  id: 'file',
  title: 'File',
  description: 'Attach a file',
  icon: '📎',
  action: function (editor) {
    // Trigger Froala's native file upload dialog
  }
}

You can also add dynamic items, such as loading a list of page templates from your API and appending them into the menu on the fly inside buildMenu.

Important Gotchas and Solutions

Problem Solution in this code
Menu closes before click registers Use mousedown + e.preventDefault()
Cursor jumps when using arrows Prevent default in keydown
Filter text remains after selection Manually edit the text node before inserting
Menu appears mid-sentence Check that nothing precedes / on the line
Editor loses focus Restore focus after action execution

Making It Production-Ready

Here are several improvements you can add:

  1. Better table insertion: Use Froala’s insertTable command instead of raw HTML when possible
  2. Command shortcuts: Add keyboard shortcuts (e.g., /h1)
  3. Icons: Replace text icons with SVG or Font Awesome for polish
  4. Async commands: Support commands that open modals (image upload, etc.)
  5. Accessibility: Add role="listbox" and aria-activedescendant
  6. Debouncing : The current implementation rebuilds on every key. For very large command sets, add light debouncing

For commands that should run through standard key combinations rather than the slash menu, review the Froala keyboard shortcut configuration and register the required command shortcut.

Alternative Approaches

You could also implement this using:

  • A separate library like Tippy.js for positioning
  • MutationObserver instead of key events (more complex)

The manual approach shown here gives you maximum control and works without additional dependencies.

Conclusion

Building a slash command menu teaches you several valuable skills:

  • Working with Selection and Range APIs
  • Managing state between a rich text editor and custom UI
  • Handling the timing quirks of contenteditable elements
  • Creating accessible, keyboard-first interfaces

The code is deliberately kept simple and self-contained so you can understand every line. Once you’re comfortable with the core mechanics, you can extend it with custom commands, better styling, or integration with your own design system.

Try it now: Paste the vanilla JS example into a local HTML file, add your license key, and open it in a browser. Type / at the start of a blank line to see the menu in action.

FAQ

Does Froala V5 have a built-in slash command menu?

No. Froala’s Quick Insert plugin shows a + button on empty lines and is triggered by the cursor position, not by typing /. The slash command UX in this guide is a custom layer built on top of Froala’s public API.

Will this break Froala’s built-in Quick Insert plugin?

No. The two features are independent. You can run both simultaneously if you want. The quickInsertEnabled option controls the + button behavior and is unrelated to the keyup event handler you are adding.

Can I add custom icons using SVG instead of emoji?

Yes. Replace the icon string in any SLASH_COMMANDS item with an SVG string, and update the .item-icon container’s innerHTML assignment in buildMenu accordingly.

How do I filter commands server-side or asynchronously? Modify buildMenu to be async and replace the SLASH_COMMANDS.forEach block with an await fetch(...) call. Pass the filter string as a query parameter. Show a loading spinner in the menu while the request is in flight.

How do I handle the slash command in Angular?

The pattern is identical. Inject the Froala config object with the same events block into your @Input() editorConfig. Access the editor instance from the initialized callback and store it in a component property. All other logic (DOM manipulation for the dropdown, text node traversal) lives in separate service methods.

What happens if the user types “/” and then presses Backspace?

The keyup handler fires after the Backspace. getSlashTriggerText re-evaluates the current text node. If the “/” has been deleted, slashIndex will be -1 and the function returns null, which triggers hideMenu(). The menu disappears automatically.

Why Inline Editor Experiences Convert Better Than Modal Ones

A glowing web application interface with a cursor clicking directly into an editable content field in place, with dashed edit borders visible around active areas, representing how an inline editor lets users edit content directly within the page without opening a separate window.

Every extra click between a user and completing a task is a chance for them to abandon it. An inline editor addresses this directly by letting users edit content in place, without interruptions or context switches. For web applications where content editing is a core activity, that difference shows up in engagement, completion rates, and retention.

The difference between editing content directly on the page versus editing it inside a separate window might sound minor. However, behaviorally, it isn’t. This article explains why inline editor experiences consistently outperform modal ones and when each approach genuinely makes sense.

Key Takeaways

  • Inline editors let users edit content in place, preserving workflow context and reducing the cognitive load of switching between views.
  • Modal editors interrupt user flow by forcing a context switch, which increases friction and raises the risk of abandonment.
  • Fewer interaction steps between intent and action directly improve task completion and conversion rates.
  • Mobile users benefit most from inline editing, since modals introduce usability and accessibility problems on small screens.
  • The two approaches aren’t mutually exclusive. Inline editing works best for quick changes, while modals still serve complex, multi-step tasks.

What Is an Inline Editor?

Inline editors represent a specific philosophy about where editing should happen: exactly where the content lives, not in a separate space.

Understanding Inline Editing

An inline editor lets users click directly on content and edit it in place. The page doesn’t change, no overlay appears, and no navigation occurs. The content becomes editable where it sits, and changes appear in real time as the user types or formats.

This approach keeps the editing experience tightly connected to the visual context. Users see how their changes affect the surrounding layout as they make them. This is in contrast to guessing from inside a detached editing window.

How Inline Editors Differ From Modal Editors

Modal editors open a separate window, overlay, or panel that sits above or away from the page content. Users edit in this detached environment and then save or confirm to apply changes back to the original view. Every modal interaction involves at least two extra steps: opening the modal and closing it.

Inline editors eliminate both steps, as the user clicks, edits, and moves on. As a result, the workflow never breaks, and the user never loses visual contact with the surrounding content.

Common Use Cases for Inline Editors

Inline editors appear across a wide range of product types. For example, CMS platforms use them to let content teams update text, images, and headlines directly on the page. Furthermore, SaaS dashboards use them for editing labels, descriptions, and field values without leaving the current view.

Website builders also rely on inline editing as their primary interaction model. Lastly, collaborative platforms use them so multiple contributors can edit shared content without triggering disruptive overlays for every change.

Why User Experience Impacts Conversion Rates

Two glowing conversion funnels side by side, the left showing a smooth unobstructed flow representing a frictionless inline editing experience, and the right showing blockages and scattered particles representing user drop-off caused by disruptive modal editing workflows.

The connection between editing interface design and conversion rates isn’t always obvious, but experiences with friction and user behavior make it clear.

Reducing Workflow Friction

Friction in a digital workflow refers to anything that slows down or complicates a user’s path to completing a task. Examples of friction include unnecessary clicks, loading transitions, and context switches. Each friction point reduces the likelihood that a user completes what they started.

Inline editors remove the most common sources of editing friction. For instance, users don’t navigate away, don’t wait for a modal to load, and don’t need to reorient themselves after closing one.

Maintaining User Focus

Modal windows are attention-splitting by design. They appear on top of the existing interface, requiring users to shift their focus away from what they were doing. This cognitive switch, even a brief one, can disrupt momentum and increase error rates.

Inline editing keeps focus on the content itself. Users stay within the same visual environment throughout the task, which helps reduce the mental overhead required to maintain context.

Speeding Up Content Editing

The speed at which a user can act on an intention directly affects how often they follow through. Inline editors compress the time between deciding to make a change and making it. For content teams managing high volumes of updates, that compression multiplies across every editing session.

Why Inline Editor Experiences Convert Better

The behavioral advantages of inline editing translate directly into measurable outcomes for products and content operations.

Inline Editing Creates a More Natural Workflow

Users expect to interact with content where they see it. Clicking on text to edit it mirrors how touchscreens and modern interfaces already work. This intuitive interaction pattern reduces the learning curve for new users and speeds up adoption inside products.

When editing feels natural, users engage with it more frequently and more confidently. As a result, hesitation decreases, and task completion rates improve.

Fewer Interruptions Increase Engagement

Every modal that appears during a workflow is a micro-interruption. Users must acknowledge it, work within it, and dismiss it before returning to their primary task. Over a full editing session, these interruptions accumulate into a noticeably degraded experience.

Inline editors eliminate the interruption cycle entirely. Editing flows continuously, and users remain engaged throughout rather than repeatedly pausing to manage overlays.

Faster Interactions Improve Conversion Opportunities

Speed is sometimes more of a behavioral metric rather than just a performance one. This means that users who can complete editing actions quickly are more likely to complete them at all. This matters especially during onboarding, where a slow or confusing editing experience can cause users to disengage before they’ve seen the product’s value.

Inline editing reduces the steps between intent and completion. In turn, users more easily submit content, update profiles, fill out forms, and complete the actions that drive conversion.

Better Mobile Editing Experiences

A desktop monitor, tablet, and phone displayed side by side, each showing the same inline editor interface with a contextual formatting toolbar adapting to its screen size, representing how an effective inline editor maintains a consistent editing experience across all devices.

Modal editors present specific problems on mobile devices. Overlays that work acceptably on a desktop can cover the entire screen on a phone. This can obscure the keyboard or require scrolling that breaks the editing context entirely. Touch interfaces also make closing and repositioning modals cumbersome.

Inline editors avoid these problems by keeping editing within the existing page layout. The interaction scales naturally to smaller screens, and touch input works the same way it does with any other content on the page.

Challenges With Modal Editor Experiences

Modal editors have genuine use cases. However, their limitations are significant enough to make them a poor default for most editing tasks.

Modal Windows Interrupt User Flow

Every modal creates a forced context switch. The user leaves the page environment, completes a task in a separate space, and returns. For simple edits, this introduces more overhead than the edit itself requires, so the interruption is disproportionate to the task.

Limited Visibility and Context

Inside a modal, users lose sight of the surrounding content. They can’t see how a change affects nearby elements, check consistency with adjacent text, or verify layout while editing. This isolation increases formatting errors and requires additional review steps after the modal closes.

Poor Mobile and Responsive Usability

On small screens, modals frequently cover the viewport, conflict with soft keyboards, and create scrolling behavior that breaks the editing experience. Accessibility tools also struggle with modal focus management, which creates barriers for users relying on screen readers or keyboard navigation.

Increased Risk of User Drop-Off

Repetitive modal interactions drain patience. Users who must open and close overlays for every minor edit will eventually stop making edits altogether. This drop-off is particularly costly in products where user-generated content or frequent updates are central to the value proposition.

Benefits of Inline Editors for Businesses

The user experience advantages of inline editors produce concrete business outcomes.

Improved Content Team Productivity

Content teams that edit inline spend less time managing the interface and more time managing content. The absence of repeated modal open-close cycles shortens editing sessions and reduces cognitive fatigue across long workdays. Non-technical users also require less training, since inline editing aligns with familiar interaction patterns.

Higher User Engagement

Products that minimize editing friction see higher engagement with content creation features. When editing feels effortless, users interact more often, update content more frequently, and stay active in the product longer. In turn, session duration and return rates both benefit.

Better Collaboration Across Teams

Inline editors support collaborative workflows naturally. Multiple contributors can make quick changes without triggering disruptive overlays for others in shared environments. Review and approval cycles also move faster when reviewers can suggest changes directly on the content rather than through a detached editing interface.

Increased Conversion and Completion Rates

Fewer steps between intent and action translate directly to higher completion rates for forms, profile updates, content submissions, and onboarding flows. Products built around inline editing typically see measurable improvements in the conversion metrics tied to those actions.

Key Features of Effective Inline Editors

Not all inline editors deliver the same experience. These are the features that separate effective implementations from frustrating ones.

Real-Time Editing and Preview

A glowing editor interface on the left with an active content element being edited, reflected instantly and identically in a mirror panel on the right, representing how real-time editing and preview keep the visual output synchronized with every change a user makes.

Changes should appear instantly as users type or apply formatting. That’s because delays between action and visual feedback break the sense of direct manipulation that makes inline editing feel natural. Real-time rendering also helps users catch errors immediately rather than after saving.

Responsive and Mobile-Friendly Design

An inline editor that works well on desktop but degrades on mobile delivers an inconsistent experience across devices. Effective implementations maintain the same interaction model regardless of screen size, adapting layout and touch targets without compromising functionality.

Clean and Minimal User Interface

Inline editors should surface formatting controls only when relevant, keeping the interface uncluttered when the user isn’t actively editing. A floating toolbar that appears on selection, rather than a persistent panel, is a common pattern that balances accessibility with visual simplicity.

Flexible Customization Options

Different products need different editing capabilities. An effective inline editor exposes configuration options that let developers restrict or extend the available tools. Editors like Froala support this through customizable toolbars and permission-based controls that adapt the editing experience to specific product contexts.

Best Practices for Designing Inline Editor Experiences

Good implementation decisions make inline editors feel seamless. Conversely, poor ones make them feel clunky despite the underlying approach being sound.

Keep Editing Controls Contextual

Show formatting tools only when a user is actively editing and hide them when they’re not. Persistent toolbars visible at all times compete for attention with the content itself. Contextual controls appear precisely when needed and disappear when the user moves on.

Prioritize Performance and Speed

An inline editor that lags when activated or stutters during typing undermines its own value proposition. The editing interface should respond instantly to user input across browsers and devices. Performance testing with realistic content volumes helps catch issues before they affect users in production.

Maintain Accessibility Standards

Inline editors must support keyboard navigation so users can activate, edit, and exit without a mouse. Screen readers need to announce when content becomes editable and when changes are saved. Accessibility failures in the editing interface create barriers for a significant portion of users and expose products to compliance risk.

Balance Simplicity and Functionality

The goal isn’t to strip an editor down to nothing but to surface the right tools at the right time. Too few options might frustrate power users, while too many overwhelm new ones. Matching the feature set to the actual editing tasks your users perform is more effective.

When Modal Editors Still Make Sense

Inline editing isn’t always the right choice. Some tasks genuinely require the focused environment a modal provides.

Advanced or Complex Editing Tasks

Large forms with many fields, detailed settings panels, and multi-step configuration flows benefit from a dedicated editing environment. Trying to accommodate these in an inline context creates clutter and confusion that a well-designed modal avoids.

Scenarios Requiring Full-Screen Focus

Long-form document editing, media-heavy workflows, and specialized administrative tasks sometimes benefit from removing the surrounding page context entirely. When the editing task is complex enough that surrounding content becomes a distraction, a full-screen or modal environment is appropriate.

Combining Inline and Modal Editing Strategically

The most effective products use both approaches where each fits best. Inline editing handles quick text changes, label updates, and minor formatting. Modals handle configuration screens, advanced settings, and multi-step workflows. Designing clear transitions between the two modes, rather than forcing all editing into one paradigm, produces the best overall experience.

Conclusion

The editing interface users encounter shapes how they feel about a product. Inline editors keep users in context, reduce friction, and compress the steps between intent and action. All these translate into higher engagement and better conversion outcomes.

Modal editors still serve specific purposes, and a product that combines both approaches thoughtfully will outperform one that commits to either extreme. Think of it this way. Instead of making editing feel like an interruption to the page, make it feel like a part of it.

Ready to bring inline editing into your product? Froala’s inline editor gives development teams a lightweight, customizable implementation that keeps users in context and reduces editing friction from day one.

Frequently Asked Questions

What is an inline editor?

An inline editor lets users click directly on content and edit it in place, without opening a separate window or overlay.

Why do inline editors improve conversions?

They reduce the steps between a user’s intent and the completion of an action. Fewer clicks, no context switching, and faster interactions mean users are more likely to finish what they started.

How are inline editors different from modal editors?

Modal editors open a separate window or overlay where users edit content in isolation from the surrounding page. Inline editors keep editing within the existing interface.

Are inline editors better for mobile users?

Generally, yes. Modals on mobile frequently cover the screen, conflict with keyboards, and require awkward scrolling. Inline editors adapt to the existing page layout and scale to smaller screens without introducing those usability problems.

When should businesses use modal editors instead of inline editors?

Modals work well for complex tasks that benefit from a dedicated editing environment. These typically include large forms, multi-step configuration flows, and administrative interfaces.

graphical user interface, text

Best Ways to Use WYSIWYG HTML Editor in 2026

A glowing content calendar filled with article, video, image, and audio icons across multiple dates, with a rocket launching from the lower left, representing the publishing velocity and content output acceleration that a WYSIWYG HTML editor enables for modern content teams.

Content teams are publishing more than ever, across more channels, with fewer technical resources. As such, the tools that support that output matter more than most teams acknowledge. A WYSIWYG HTML editor sits at the center of that conversation, giving writers, marketers, and developers a shared environment.

These editors continue to evolve with web applications and content platforms. For instance, modern ones support real-time collaboration, no-code workflows, AI-assisted formatting, deep API integrations, and so on. In this article, you’ll learn the best ways to use WYSIWYG HTML editors in 2026.

Key Takeaways

  • WYSIWYG HTML editors reduce technical barriers by letting non-developers produce structured, publish-ready content independently.
  • The highest-value use cases in 2026 include CMS publishing, email campaign production, landing page management, and user-generated content platforms.
  • Modern editors increasingly support AI assistance, real-time collaboration, and headless CMS integrations that extend their usefulness beyond basic text formatting.
  • Best practices like clean HTML output, role-based toolbar customization, and content sanitization directly affect SEO performance and application security.
  • Teams that configure their editor to match their workflow rather than using default settings see measurably better adoption and content consistency.

What Is a WYSIWYG HTML Editor?

WYSIWYG editors have been around for decades, but the version available in 2026 looks very different from early implementations. Understanding the fundamentals helps clarify why they remain a central tool in modern content workflows.

Understanding WYSIWYG Editing

WYSIWYG stands for “What You See Is What You Get.” The editing surface mirrors the published output directly, so users format content visually without writing or reading HTML. For example, clicking “bold” automatically generates a <strong> tag in the background as users work.

This contrasts with code editors, where users write markup directly and see the rendered result only after opening it in a browser. WYSIWYG editors help close that gap, making the editing surface and the output surface the same thing.

Why WYSIWYG HTML Editors Remain Important in 2026

Content velocity has increased, and the expectation that developers handle every formatting task is no longer practical. WYSIWYG editors give content teams the autonomy to draft, format, and publish without filing a ticket or waiting for a code review. For organizations publishing at volume, that independence translates directly into faster turnaround and lower operational cost.

Common Features of Modern WYSIWYG HTML Editors

Modern editors include rich text formatting, covering headings, lists, alignment, tables, and inline styles. Furthermore, media embedding through drag-and-drop handles images and video without external tools. Additionally, a real-time preview shows rendered output as users type. Collaboration features allow multiple contributors to work simultaneously, and customization options let developers configure toolbars and permissions to match specific product contexts.

Best Ways to Use a WYSIWYG HTML Editor in 2026

The right use cases determine how much value an editor actually delivers. These are the workflows where WYSIWYG editors produce the clearest return.

Creating Website and Landing Page Content

Marketing teams managing landing pages and website sections benefit immediately from visual editors. Copy changes, image swaps, and layout adjustments happen directly in the editor without touching templates or HTML files. Campaign timelines compress when a marketer can publish a page update independently rather than routing it through a developer.

Managing Content in CMS Platforms

CMS-based editorial workflows are where WYSIWYG editors spend most of their time. Writers draft and format articles, editors review and adjust, and content publishes with consistent structure throughout. The editor enforces formatting standards automatically, so heading hierarchy and paragraph spacing stay uniform regardless of who authored each piece.

Building Email and Newsletter Campaigns

A visual email builder interface on a desktop showing a drag-and-drop layout editor with image and text blocks, alongside a mobile preview of the same email rendered responsively, representing how a WYSIWYG editor simplifies email and newsletter campaign production.

Email HTML is notoriously finicky, requiring precise markup that renders correctly across dozens of clients. A WYSIWYG editor in an email builder abstracts that complexity. It lets marketers design visually while the editor generates the tight, table-based HTML that email rendering engines require. The result is faster campaign production with fewer rendering surprises.

Supporting User-Generated Content

Applications that allow users to submit formatted content, comments, documentation, or posts need an editor that’s simple enough for non-technical contributors. A WYSIWYG editor provides that interface while the application handles sanitization and storage of the HTML output. The editing experience stays familiar, while the technical complexity stays invisible.

How Businesses Use WYSIWYG HTML Editors in 2026

The pattern is consistent across different industries. Teams that integrate WYSIWYG editors into their workflows tend to reduce handoffs, publish faster, and produce more consistent output.

Enhancing Collaboration Across Teams

When marketers, designers, and developers share the same editor, review cycles shorten. A designer can adjust a layout directly, while a marketer can update copy without a developer intermediary. An editor can also approve changes in context. Each removed handoff reduces delay and the risk of communication errors.

Accelerating Digital Publishing

Speed-to-publish affects competitive positioning in content marketing, e-commerce, and SaaS. Teams using WYSIWYG editors that integrate directly with their CMS or publishing pipeline eliminate the export-import cycle that slows manual workflows. In turn, content decisions translate to live pages faster.

Supporting No-Code and Low-Code Workflows

Organizations reducing their dependency on engineering resources use WYSIWYG editors to push more content management responsibility to non-technical teams. Product managers update in-app copy, marketing teams manage campaign pages, and support teams maintain knowledge bases. Each shift reduces the engineering backlog without sacrificing output quality.

Delivering Better User Experiences

Content quality affects how users perceive a product. Consistent heading hierarchy, properly embedded media, and clean formatting produce pages that are easier to read and navigate. WYSIWYG editors that output semantic HTML also support accessibility tools, improving the experience for users relying on screen readers.

Key Features Modern WYSIWYG HTML Editors Need in 2026

The baseline feature set that made editors useful five years ago might not suffice for the workflows teams run today. These are the capabilities that matter most now.

AI-Assisted Content Editing

A desktop monitor displaying a WYSIWYG editor interface with an AI assistant panel open on the right side, showing formatting suggestions and editing actions overlaid on the active content, representing how AI-assisted features help content teams reduce manual editing effort.

AI features in 2026 extend beyond spell-check. Modern editors use AI to suggest formatting improvements, detect structural inconsistencies, and automate repetitive tasks like generating alt text for images or summarizing long-form content. Teams that take advantage of these features drastically reduce the manual effort per piece. However, it’s important to check all AI output for inconsistencies or errors.

Responsive and Mobile-First Editing

Content teams work across devices, and the editing experience needs to match. An editor that functions well on desktop but degrades on a tablet can create inconsistency for distributed teams. Mobile-first design in an editor means touch interactions work naturally, toolbars adapt to screen size, and the editing surface remains usable regardless of device.

Real-Time Collaboration Features

Distributed teams need simultaneous editing, visible cursor positions, and in-context commenting. These features reduce the back-and-forth of asynchronous review cycles and keep all contributors working from the same version of the content. Editors without real-time collaboration require workarounds that introduce version control problems.

Flexible API and Integration Support

An editor that can’t connect to your CMS, storage layer, or publishing pipeline creates a bottleneck at the integration point. Strong API support and documented webhooks allow editors to fit inside existing workflows rather than requiring teams to build around them.

Best Practices for Using a WYSIWYG HTML Editor

Configuration and usage habits determine whether an editor delivers its potential or creates new problems. Before applying these best practices, make sure your team is choosing the best WYSIWYG HTML editor for your workflow, content structure, security needs, and integration requirements.

Keep Content Structure Clean

Use heading levels in sequence rather than selecting heading sizes for visual effect. Consistent structure makes content easier to scan, improves accessibility, and produces HTML that search engines interpret correctly.

Optimize Media and Performance

Large uncompressed images embedded through the editor could inflate page load times. Configure the editor to resize or compress images on upload, or connect it to a media pipeline that handles optimization automatically. It’s easier to prevent performance issues stemming from editor-embedded media than troubleshoot them in the future.

Customize the Editing Experience

Default editor configurations typically expose every available tool, which overwhelms non-technical users and clutters the interface for focused workflows. Configure role-based toolbars that surface only the controls each user type actually needs. Different types of users may not need the same tools.

Prioritize Security and Content Sanitization

Content from users is a security risk if the editor doesn’t sanitize its output. Configure the editor to strip disallowed tags and attributes on input, and validate the output server-side before storing or rendering it. Relying only on client-side sanitization leaves an exploitable gap, since users can bypass browser-side controls through direct API requests.

Common Mistakes to Avoid When Using WYSIWYG HTML Editors

Most editor problems stem from configuration choices rather than editor limitations.

Overloading Editors With Features

A split comparison showing an overcrowded editor toolbar packed with dozens of icons on the left, contrasted with a clean minimal toolbar containing only six essential tools on the right, illustrating the usability difference between an overloaded and a properly configured WYSIWYG editor.

Exposing every available tool by default creates cognitive overload for non-technical users and slows adoption. Start with a minimal configuration and add tools as specific needs arise. A focused toolbar helps drive faster onboarding and more consistent usage patterns.

Ignoring Mobile Editing Experiences

Testing the editor only on desktop misses how distributed teams actually work. Instead, test the editing experience on real mobile devices, not just browser responsive mode. This is because touch interactions, toolbar positioning, and keyboard behavior all behave differently on physical devices.

Neglecting SEO and Content Structure

Content with non-semantic HTML often undermines search performance. Headings chosen for visual size rather than document hierarchy confuse web crawlers. Furthermore, alt text left empty on images reduces accessibility and indexing. 

Failing to Train Teams Properly

An editor is only as effective as the people using it. Establish formatting guidelines, document which toolbar options each role should use, and provide brief onboarding for new contributors. Inconsistent usage produces inconsistent output, which requires cleanup work that negates the efficiency gains the editor was meant to deliver.

Future Trends for WYSIWYG HTML Editors

The direction editors are heading in 2026 reflects broader shifts in how teams build, publish, and manage digital content.

AI-Powered Editing and Automation

At the rate it’s going, AI integration will most likely deepen. Editors will increasingly suggest structural improvements in real time, flag readability issues, and automate tasks like tagging, categorization, and formatting normalization. Teams that adopt these features early could reduce per-piece production time significantly.

Deeper Collaboration Capabilities

Real-time collaboration features might expand beyond simultaneous editing. Expect tighter integration with approval workflows, comment resolution tracking, and change history that connects to content governance systems. Distributed teams will benefit most from these developments.

Headless CMS and Embedded Editing Growth

As composable architectures become standard, embedded editors will increasingly serve as the content layer inside headless CMS setups. Rather than using a CMS-native editor, teams could embed a configurable editor component directly into their front-end applications. This can give developers more control over the editing experience while keeping content operations accessible to non-technical users.

Greater Focus on Accessibility and Inclusivity

Web accessibility requirements are tightening in many jurisdictions. So, editors will need to produce accessible HTML by default, including correct heading structure, image alt text, and ARIA attributes. Teams that adopt editors aligned with WCAG standards now will face less remediation work as requirements evolve.

Conclusion

A WYSIWYG HTML editor is perhaps one of the highest-leverage investments a content operation can make, but only when teams use it deliberately. The editor’s value comes from configuration, workflow integration, and consistent usage habits, not from the feature list alone.

The use cases, best practices, and trends covered here reflect where content operations could be heading in 2026. Teams that match their editor to their workflow produce better content faster. Ultimately, the early decisions will determine whether the editor works for the team or the team works around the editor.

Looking for a WYSIWYG HTML editor built for modern content workflows? Froala offers a lightweight, highly configurable editor with clean HTML output, real-time collaboration, and flexible API support.

Frequently Asked Questions

What is a WYSIWYG HTML editor?

A WYSIWYG HTML editor is a visual content editing tool that lets users format text, embed media, and structure content. It does so with an interface that mirrors the published output.

Why are WYSIWYG HTML editors important in 2026?

They allow non-technical team members to create and publish structured web content without developer support. As content velocity increases and engineering resources stay constrained, editors that reduce technical dependency become a practical operational advantage.

How do businesses use WYSIWYG HTML editors?

Businesses use them across CMS publishing, email campaign production, landing page management, user-generated content platforms, and internal documentation. The common thread is reducing the number of people and steps required to move content from draft to live.

What features should modern WYSIWYG HTML editors include?

Essential features in 2026 include clean semantic HTML output, real-time collaboration, responsive mobile editing, AI-assisted formatting, flexible API support, and role-based customization.

Are WYSIWYG HTML editors suitable for non-technical users?

Yes. The visual interface handles all formatting through familiar controls, and the HTML generates automatically. Non-technical users can produce structured, publish-ready content without understanding markup, provided the editor is configured with an appropriate toolbar for their role.

graphical user interface, text

Free In-Editor Image Editing for Froala with Filerobot Plugin

Advanced Image editing

Froala v5.2.0 ships with a new plugin that brings a full-featured image editor directly into your editor workflow. This guide walks you through everything you need to know about the Filerobot Image Editor (FIE) Plugin: what it does, how to install it, all of its configuration options, its event hooks, and how it compares to the other advanced image editing integrations Froala supports.

Key Takeaways

  • The Filerobot plugin is free, open-source, and requires no third-party account or API key.
  • It adds an “Advanced Edit” button to Froala’s image toolbar, opening a full editing suite (annotations, filters, fine-tune, resize, rotate, watermark, crop) as a modal without leaving the editor.
  • The plugin ships with two configuration options (filerobotOptions and FilerobotImageEditor) and five lifecycle events (beforeOpen, opened, beforeSave, saved, closed) giving you precise control over the editing experience.
  • Froala offers two other advanced image editing integrations — Image TUI (free, open-source) and Filestack Transformation (cloud-based, paid) — covered in the Choosing the Right Integration section below.

What Is the Filerobot Image Editor Plugin?

The Filerobot Image Editor Plugin integrates Scaleflex’s Filerobot Image Editor directly into Froala. When a user selects an image in the editor, a new “Advanced Edit” button appears in the image toolbar. Clicking it opens a full editing suite as a modal overlay without leaving the editor context.

The editing suite supports:

  • Annotations — draw shapes, add text, arrows, and freehand lines directly on the image
  • Filters — apply Instagram-style visual filters
  • Fine-tune — adjust brightness, contrast, saturation, and sharpness
  • Resize — change image dimensions with aspect ratio control
  • Rotate & Flip — rotate in configurable increments, flip horizontally or vertically
  • Watermark — overlay a logo or text watermark
  • Crop — crop to a custom ratio or preset dimensions

When the user clicks Save, the edited image replaces the original inline without page reload or manual re-upload step.

Choosing the Right Image Editing Integration

Before diving into setup, it’s worth knowing that Froala offers three different pathways for advanced in-editor image editing. Each targets a different set of requirements:

Filerobot Image Editor Plugin (new in v5.2.0): Free and open-source. Actively maintained by Scaleflex. Delivers a rich, modern editing UI with annotations, filters, fine-tuning, watermarks, and resize out of the box. No account or API key needed. A solid choice when you want a capable, dependency-light solution with no ongoing service cost.

Image TUI Plugin: Free and open-source (MIT). Built on TOAST UI Image Editor and bundled with Froala as imageTUI. Provides filters, cropping, drawing, and sticker support. Worth noting that the upstream TUI Image Editor project has not seen active maintenance in some time, which may be a consideration for long-term projects.

Filestack Transformation (via the Filestack Plugin): A commercially integrated solution that pairs Froala with Filestack’s cloud-based transformation pipeline. For applications that need more automated image processing, smart cropping in a WYSIWYG editor can use Froala and Filestack to identify important areas of an image and generate appropriately cropped versions with less manual adjustment. Delivers powerful server-side image processing and CDN delivery alongside Filestack’s file picker and storage capabilities. Requires a Filestack account and subscription. The right fit when you’re already in the Filestack ecosystem or need cloud-native image processing at scale.

All three integrate cleanly into Froala’s toolbar without requiring custom UI work on your part. For a practical walkthrough of image editing in a WYSIWYG editor, you can also explore how Froala works with Filestack to support cropping, filters, text, borders, and other image transformations. The choice comes down to your project’s constraints around cost, maintenance expectations, and infrastructure.

Filerobot Image Editor Setup

Step 1: Install the Plugin Assets

The Filerobot plugin ships inside the froala-editor npm package as of v5.2.0.

Via npm:

npm install froala-editor@latest

Plugin files (local paths after npm install):

# JavaScript
node_modules/froala-editor/js/third_party/imageFileRobot.min.js

# CSS
node_modules/froala-editor/css/third_party/imageFileRobot.min.css

Via CDN (for plain HTML projects):

<!-- Filerobot Image Editor dependency — load this first -->
<script src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"></script>

<!-- Froala core -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css">
<script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>

<!-- Filerobot plugin -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/third_party/imageFileRobot.min.css">
<script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/third_party/imageFileRobot.min.js"></script>

Important: The Filerobot Image Editor script from Scaleflex must be loaded before the Froala plugin script. The plugin expects window.FilerobotImageEditor to exist at initialization time. If you load them in the wrong order, the plugin will silently fail to open.

Froala Filerobot plugin setup

Step 2: Basic Setup (Vanilla JS)

The minimum configuration to get the plugin running:

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Filerobot Image Editor (load before Froala plugin) -->
  <script src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"></script>

  <!-- Froala core assets -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css">
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>

  <!-- Filerobot plugin assets -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/third_party/imageFileRobot.min.css">
  <script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/third_party/imageFileRobot.min.js"></script>
</head>
<body>
  <div id="editor">
    <p>Insert an image to try the advanced editor.</p>
  </div>

  <script>
    new FroalaEditor('#editor', {
      // Enable both the core image plugin and the Filerobot plugin
      pluginsEnabled: ['image', 'imageFilerobot'],

      // Your Froala license key
      key: 'YOUR_FROALA_LICENSE_KEY'
    });
  </script>
</body>
</html>

That’s all it takes for a working integration. Once an image is inserted and selected, you’ll see the Advanced Edit button in the image toolbar.

Step 3: Configuration Options

The plugin exposes two options under the main Froala options object.

filerobotOptions (Object)

This option is passed directly to the Filerobot Image Editor instance and controls the editor’s UI and behavior. The full default configuration Froala ships with is:

filerobotOptions: {
  theme: {
    palette: {
      'bg-primary-active': '#4CA6FF'  // Accent color for active UI elements
    },
    typography: {
      fontFamily: 'Roboto, Arial'
    }
  },
  annotationsCommon: {
    fill: '#ff0000'  // Default fill color for annotations
  },
  Text: { text: 'Enter text...' },       // Default text for text annotations
  Rotate: { angle: 90, componentType: 'buttons' },  // Rotation step and control type
  tabsIds: ['Adjust', 'Annotate', 'Filters', 'Finetune', 'Resize', 'Watermark'], // Visible tabs
  defaultTabId: 'Annotate',   // Tab shown when the editor opens
  defaultToolId: 'Text',      // Tool selected on load within the default tab
  savingPixelRatio: 0,        // 0 = use original image pixel ratio
  previewPixelRatio: 0,       // 0 = use screen pixel ratio for preview
  closeAfterSave: true,       // Close the editor modal automatically after saving
  defaultSavedImageType: 'png' // Output format: 'png', 'jpeg', or 'webp'
}

You override only the properties you need — Froala merges your config with the defaults.

FilerobotImageEditor (Object)

Use this option when the Filerobot library is not available on window — for example, when you import it as an ES module rather than loading it via a <script> tag.

import FilerobotImageEditor from 'filerobot-image-editor';

new FroalaEditor('#editor', {
  pluginsEnabled: ['image', 'imageFilerobot'],
  // Pass the constructor directly instead of relying on window.FilerobotImageEditor
  FilerobotImageEditor: FilerobotImageEditor
});

Default: window.FilerobotImageEditor

Step 4: Practical Configuration Examples

Restrict the editor to only Crop and Resize

new FroalaEditor('#editor', {
  pluginsEnabled: ['image', 'imageFilerobot'],
  key: 'YOUR_FROALA_LICENSE_KEY',

  filerobotOptions: {
    // Only show Adjust and Resize tabs — remove annotation and filter clutter
    tabsIds: ['Adjust', 'Resize'],
    defaultTabId: 'Adjust',
    defaultToolId: 'Crop',

    // Save as JPEG for smaller file sizes
    defaultSavedImageType: 'jpeg',

    // Keep the editor open after saving so users can make additional adjustments
    closeAfterSave: false
  }
});

Why this matters: Restricting tabs reduces cognitive overhead for users who only need basic adjustments. It also reduces the initial render cost of the editor modal since unused tool panels are never instantiated.

Custom theme to match your app’s branding

new FroalaEditor('#editor', {
  pluginsEnabled: ['image', 'imageFilerobot'],
  key: 'YOUR_FROALA_LICENSE_KEY',

  filerobotOptions: {
    theme: {
      palette: {
        'bg-primary-active': '#6C47FF',  // Your brand purple
        'bg-secondary': '#F5F5F5'
      },
      typography: {
        fontFamily: 'Inter, sans-serif'
      }
    },
    annotationsCommon: {
      fill: '#6C47FF'  // Match annotation color to your brand
    },
    tabsIds: ['Adjust', 'Annotate', 'Filters', 'Finetune', 'Resize', 'Watermark'],
    defaultTabId: 'Filters'  // Open on Filters tab by default
  }
});

Using the ES module import (for bundled projects)

// Import the Filerobot Image Editor as a module
import FilerobotImageEditor from 'filerobot-image-editor';

// Import Froala and the plugin
import FroalaEditor from 'froala-editor';
import 'froala-editor/js/plugins/image.min.js';
import 'froala-editor/js/third_party/imageFileRobot.min.js';
import 'froala-editor/css/froala_editor.pkgd.min.css';
import 'froala-editor/css/third_party/imageFileRobot.min.css';

new FroalaEditor('#editor', {
  pluginsEnabled: ['image', 'imageFilerobot'],
  key: 'YOUR_FROALA_LICENSE_KEY',

  // Provide the class directly — no window global needed
  FilerobotImageEditor: FilerobotImageEditor,

  filerobotOptions: {
    tabsIds: ['Adjust', 'Annotate', 'Filters', 'Resize'],
    defaultTabId: 'Annotate',
    defaultSavedImageType: 'png'
  }
});

Event Reference

The plugin emits five lifecycle events you can hook into:

Event Signature When It Fires
filerobot.beforeOpen () Just before the editor modal renders
filerobot.opened (filerobotInstance) After the modal is fully rendered and interactive
filerobot.beforeSave (editedImageObject) Before the edited image is committed to the editor
filerobot.saved (editedImageObject) After the edited image replaces the original in the editor
filerobot.closed () After the editor modal is destroyed

The editedImageObject passed to filerobot.beforeSave and filerobot.saved contains the edited image data and metadata from the Filerobot editor. You can use filerobot.beforeSave to intercept the save — for example, to upload the image to your own server first, or to apply additional server-side processing before the image src is updated in the editor.

events: {
  'filerobot.beforeSave': function (editedImageObject) {
    // Log the object shape to understand what's available
    console.log(editedImageObject);

    // Example: prevent the default save behavior by returning false,
    // then handle the upload yourself
    // return false;
  },

  'filerobot.saved': function (editedImageObject) {
    // Track edits in your analytics or audit log
    trackImageEdit({ timestamp: Date.now(), imageData: editedImageObject });
  }
}

Conclusion

The Filerobot Image Editor Plugin is a meaningful addition to Froala’s image management capabilities. It gives your users a modern, fully-featured editing suite without leaving the editor — and without adding a paid service dependency. The event lifecycle is clean, the configuration API maps directly to the upstream Filerobot options, and the ES module import path makes it easy to integrate into any modern bundled project.

Whether the Filerobot plugin, Image TUI, or Filestack Transformation is the right fit depends on your specific requirements around maintenance expectations, infrastructure, and budget. All three are first-class Froala integrations with clear documentation and stable APIs.

Try Froala’s Filerobot plugin in your editor setup and give users a faster way to annotate, crop, resize, watermark, and save images without leaving the editing workflow.

FAQ

What image formats does the plugin support as output?

The plugin outputs png, jpeg, or webp. Control this with the defaultSavedImageType option inside filerobotOptions. PNG is the default. Use jpeg when file size matters more than transparency support, and webp for modern browsers where you want both compression and quality.

Will the edited image be re-uploaded to my server automatically?

No. By default, the plugin replaces the image’s src attribute in the editor with a base64 data URL of the edited image. If you need the edited image uploaded to your server, use the filerobot.beforeSave or filerobot.saved events to intercept the save, upload the image yourself, and update the src with the returned URL before it’s committed to the editor content.

Can I limit which editing tools are available to users?

Yes. Use the tabsIds property inside filerobotOptions to specify exactly which tabs appear. For example, passing tabsIds: ['Adjust', 'Resize'] removes Annotations, Filters, Fine-tune, and Watermark entirely. You can also control which tool is active on open via defaultTabId and defaultToolId.

The “Advanced Edit” button isn’t appearing in the image toolbar. What’s wrong?

Check three things in order. First, confirm that both 'image' and 'imageFilerobot' are present in your pluginsEnabled array. The Filerobot plugin won’t register without the core image plugin.

Second, verify that the Scaleflex script loads before the Froala plugin script; if window.FilerobotImageEditor is undefined at plugin initialization time, the button won’t render.

Third, if you’re using a bundler, make sure you’re passing the FilerobotImageEditor constructor explicitly via the FilerobotImageEditor option rather than relying on the window global.

Does the plugin work with images hosted on external domains?

It depends on the CORS configuration of the image host. The Filerobot editor draws the image onto an HTML5 canvas to apply edits. Browsers block canvas operations on images from cross-origin sources that don’t send Access-Control-Allow-Origin headers. If the image host doesn’t support CORS, the editor will open but throw a security error when attempting to render the image on canvas. The fix is either to configure your image server to send the appropriate CORS headers or to proxy the image through your own origin.

Can I use this plugin alongside the Image TUI or Filestack plugins at the same time?

Technically you can load multiple image editing plugins, but activating more than one will add multiple “Advanced Edit”-style buttons to the image toolbar, which creates a confusing user experience. Choose one image editing integration per project and enable only that plugin.

Further Reading

graphical user interface, text

Building a Multi-Model AI Assistant in Froala Editor

multi-model AI assistant

Froala Editor’s AI Assist feature gives developers a clean way to bring generative AI directly into the editing experience. While some users work well with a single model, others need more flexibility. Different models excel at different tasks. Some are faster and cheaper, others produce higher-quality long-form content, and some handle specific languages or coding tasks better.

This article explains how to connect multiple AI models to Froala and let users switch between them directly from the toolbar. We will walk through a working prototype that demonstrates the approach using only Froala’s configuration system and its command registration API.

switching between AI models

Key Takeaways

  • Froala’s aiAssistRequest option can be swapped at runtime, enabling multi-model support without recreating the editor.
  • A custom dropdown command in the toolbar lets users switch models seamlessly.
  • Request handler functions follow a consistent interface, so switching models requires only updating a single function reference.
  • Production deployments should proxy AI requests through a backend to protect API keys and enforce security policies.
  • Froala’s popup system lets you expose advanced settings like temperature and token limits alongside model selection.

How Froala AI Assist Works Under the Hood

Froala exposes the aiAssistRequest configuration option. This option accepts a function that receives an object containing the user’s instruction and the current selection (or full document content). The function is responsible for returning a string of HTML that Froala will insert into the editor.

Because aiAssistRequest lives in the editor’s options object, you can change it at runtime. This single fact makes multi-model support possible without touching Froala’s core code.

In the prototype we keep a map of model names to request functions:

const aiReqHandler = {
  chatgpt: openAIRequest,
  claude: claudeRequest,
  deepseek: deepSeekRequest,
  gemini: geminiRequest,
  groq: groqRequest,
  openrouter: openRouterRequest
};
 At initialization we point aiAssistRequest at the default handler:
new FroalaEditor("#editor", {
  toolbarButtons: [
    "bold",
    "italic",
    "underline",
    "|",
    "aiAssist",
    "aiShortCuts",
    "aiModelList"
  ],
  aiAssistRequest: aiReqHandler[aiDefaultModel]
  // ...
});

The key insight is that we never replace the entire editor instance. We simply swap the function reference stored in this.opts.aiAssistRequest.

Extending the Toolbar with a Custom Dropdown

Froala’s toolbar is built from registered commands. To add a model selector, we register a new command of type dropdown.

Defining a Custom Icon

First we create a recognizable icon using DefineIcon. The command uses an SVG that visually suggests multiple models:

FroalaEditor.DefineIcon("aiModel", {
  template: "svgMultiplePath",
  VIEWBOX: "0 0 48 48",
  PATHS: "..." // (SVG paths)
});

Using a custom icon keeps the toolbar consistent with Froala’s design language and makes the new control immediately understandable to users.

Registering the Dropdown Command

The actual control is created with RegisterCommand:

FroalaEditor.RegisterCommand("aiModelList", {
  title: "Select AI Model",
  type: "dropdown",
  icon: "aiModel",
  options: AI_MODELS,
  undo: true,
  focus: true,
  refreshAfterCallback: true,
  callback: function (cmd, val) {
    this.opts.selectedAiModel = val;
    this.opts.aiAssistRequest = aiReqHandler[val];
  },
  refreshOnShow: function ($btn, $dropdown) {
    const current = this.opts.selectedAiModel || aiDefaultModel;
    $dropdown.find("a").removeClass("fr-active");
    $dropdown.find(`a[data-param1="${current}"]`).addClass("fr-active");
  }
});

Several details are worth highlighting for Froala developers:

  • options is a plain object. The keys become the values passed to the callback; the values become the visible labels.
  • The callback receives the selected key (val). Inside the callback we update two properties on this.opts. This works because Froala reads aiAssistRequest again the next time the user triggers an AI action.
  • refreshOnShow runs every time the dropdown opens. It is the correct hook for applying visual state (the active model) without affecting editor performance.
  • We store the current model in this.opts.selectedAiModel so other parts of the integration (or future custom popups) can read the active choice.

Once the command is registered, we simply add its name to the toolbarButtons array. No additional plugin registration or CSS work is required.

Switching Models at Runtime

When the user selects a different model, the callback executes immediately:

this.opts.aiAssistRequest = aiReqHandler[val];
console.log(`AI Model switched to ${AI_MODELS[val]}`);

The next time the user clicks the built-in “AI Assist” or “AI Shortcuts” buttons, Froala calls whatever function currently lives at this.opts.aiAssistRequest. Because the request functions all follow the same contract (they accept the same data object and an optional AbortSignal), the rest of the AI Assist pipeline remains unchanged.

This pattern gives you several advantages:

  1. Zero downtime switching — The editor does not need to be destroyed and recreated.
  2. Per-instance state — Each Froala instance can track its own selected model.
  3. Easy debugging — You can log or expose the current handler for development tools.

You can see the complete working demo on JSFiddle. This fiddle includes all the code from this article—model registration, dropdown setup, and request handlers—running in a live editor. You can fork it, modify the models, adjust settings, or test with your own API keys.

Adding Configuration Options with Froala’s Custom Popups

A dropdown is sufficient for model selection, but many teams also want to expose parameters such as temperature, max tokens, or system prompts. Froala’s popup system lets you create rich configuration interfaces that feel native.

You can register a second command that opens a custom popup. Inside the popup you can render form controls (temperature slider, token limit input, etc.). When the user saves the popup, you store the values on this.opts (for example this.opts.aiSettings = { temperature: 0.7, maxTokens: 2048 }).

Your request functions can then read these values before making the API call:

// Inside any request handler
const settings = this.opts.aiSettings || {};
const temperature = settings.temperature ?? 0.7;

Because the popup is built with Froala’s own popup API (this.popups.create, this.popups.show, etc.), it automatically respects the editor’s theme, positioning, and keyboard navigation. This keeps the user experience consistent even when you add advanced AI controls.

A practical tip: keep the popup data minimal. Store only what the request function actually needs. Heavy configuration objects can be moved to a separate module that the request handlers import.

Why the Prototype Keeps Logic in the Frontend

The provided code performs all AI calls directly from the browser using fetch. This approach is intentional for a demonstration:

  • It shows the complete flow without requiring a backend server.
  • It lets developers quickly test different providers and models.
  • It makes the integration easy to copy into a CodePen or local HTML file.

However, this pattern must not be used in production. There are several non-negotiable reasons:

API key exposure

Embedding real API keys in client-side JavaScript makes them available to anyone who opens the browser’s developer tools. Once leaked, keys can be abused, leading to unexpected costs or account suspension.

CORS restrictions

Some providers (notably Anthropic) block direct browser requests. Even when CORS is allowed, many organizations prefer not to open their AI endpoints to arbitrary origins.

Centralized control and observability

A backend proxy gives you a single place to implement rate limiting, user authentication, cost tracking, content moderation, and prompt sanitization. You can also rotate keys or switch providers without touching the frontend.

Compliance and data residency

Many teams must ensure that sensitive document content never leaves their infrastructure in raw form. A backend layer lets you apply logging policies, PII redaction, or regional routing before any data reaches an external model.

Future flexibility

When you later add features such as streaming responses, retry logic with exponential backoff, or model fallbacks, these concerns are much easier to manage on the server.

The recommended production pattern is therefore simple: keep the model selection UI and the call to aiAssistRequest in the frontend, but have the actual request function call an internal endpoint (for example /api/ai/generate). That endpoint receives the model identifier, the prompt data, and any settings, then proxies the request to the chosen provider using server-side secrets.

Learn more on this “setup AI React WYSIWYG editor powered by Claude” guide.

Recommended Architecture for Production

  1. Store the mapping of friendly model names to provider details on the backend.
  2. Expose a single /ai-assist endpoint that accepts { model, prompt, text, settings }.
  3. Validate the incoming model against an allow-list.
  4. Apply any business rules (user quotas, content filters).
  5. Forward the request to the appropriate provider using the correct SDK or HTTP client.
  6. Return only the generated HTML (or an error object) to the editor.

On the Froala side, your aiAssistRequest function becomes very small:

async function aiAssistRequest(data, signal) {
  const response = await fetch("/api/ai-assist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: this.opts.selectedAiModel,
      prompt: data.prompt,
      text: data.text,
      settings: this.opts.aiSettings
    }),
    signal
  });
  // handle response...
}

This keeps the editor configuration clean while moving all sensitive logic behind your own authentication boundary.

Best Practices When Working with Froala’s Command System

  • Always use refreshOnShow (not refresh) when you need to update dropdown appearance. The former runs only when the menu is about to open.
  • Store runtime state on this.opts rather than on the command object itself. This keeps state scoped to the editor instance.
  • If you need to disable the model selector in certain contexts (read-only mode, specific user roles), use the refresh callback on the command to toggle the disabled class on the button.
  • Consider persisting the user’s last chosen model in localStorage and restoring it when the editor initializes. This small UX improvement makes repeated sessions feel more personal.

Testing the Multi-Model AI Integration

When testing multi-model support, verify the following flows:

  • Switching models while text is selected should use the newly chosen model on the next AI action.
  • The active model should remain highlighted when the dropdown reopens.
  • Changing models should not clear the current selection or undo stack.
  • Error states returned by one provider should not affect subsequent calls when the user switches models.

Because each request function is independent, you can also temporarily replace any handler with a mock during automated tests:

editor.opts.aiAssistRequest = async () => '<p>Mock response</p>';

Conclusion

Froala’s AI Assist configuration combined with the command registration system gives developers a surprisingly powerful extension surface. By treating aiAssistRequest as a swappable strategy and using a custom dropdown command for model selection, you can deliver a first-class multi-model experience with very little code.

The pattern scales well: whether you’re connecting two models or ten, the core architecture remains unchanged. The separation between frontend UI (model selection, settings) and backend logic (API calls, security) keeps your codebase maintainable as you add new providers or features.

Ready to get started? Fork the JSFiddle demo, connect your own API keys, and experiment with different models in your editor. Then move to a backend architecture following the recommended production pattern, and you’ll have a robust, secure multi-model AI assistant built right into your editing experience.

Try Froala Editor with AI Assist today.
graphical user interface, text

HTML WYSIWYG vs Code Editors: Which Is Better for Modern Content Teams?

HTML WYSIWYG vs Code Editors: Which Is Better for Modern Content Teams?

Content creation sits at the center of nearly every digital-first organisation, and the tools teams use to produce that content shape how fast they move, how consistently they publish, and how effectively they collaborate. Yet the conversation about which tools to use rarely gets the attention it deserves.

Two approaches dominate HTML content editing: the visual, toolbar-driven experience of an html wysiwyg editor, and the code-first environment of a traditional code editor. Both are capable tools. But they serve genuinely different users, and choosing the wrong one, or failing to set up a workflow that accommodates both, creates friction that compounds over time.

This article compares both approaches across the dimensions that matter most to modern content teams: ease of use, speed, collaboration, SEO, security, and long-term scalability. The goal isn’t to declare a winner, but to help you make the right call for your specific context.

Key Takeaways

  • WYSIWYG editors prioritise accessibility and speed; they’re built for content creators, marketers, and non-technical contributors who need to publish without writing code.
  • Code editors provide maximum control and precision; they’re the right tool for developers who need fine-grained markup management and custom implementations.
  • Collaboration is significantly easier with WYSIWYG editors, which remove the technical barrier for non-developers to participate in content workflows.
  • Both tools can support strong SEO; the difference is whether optimisation is driven by content teams (WYSIWYG) or developers (code editor).
  • Many modern organisations benefit from a hybrid model, where a WYSIWYG editor like Froala provides visual editing with an accessible HTML source view for technical users.

Understanding HTML WYSIWYG Editors

Before comparing the two approaches, it helps to understand exactly what each one does and who it’s designed for.

What Is an HTML WYSIWYG Editor?

WYSIWYG stands for “What You See Is What You Get.” A WYSIWYG editor renders content visually as you create it, meaning what you see on screen closely matches what will appear when published. There’s no gap between authoring and output: bold text looks bold, images appear in position, headings are clearly distinguished from body copy.

Behind the scenes, the editor generates valid HTML automatically. The user never has to write <strong> or <h2> tags manually; those are handled by the toolbar or keyboard shortcuts. The result is a content creation experience that feels more like a word processor than a development environment.

How HTML WYSIWYG Editors Work

The core of a WYSIWYG editor is its toolbar. Buttons for formatting (bold, italic, lists, headings), media insertion (images, videos, embeds), and layout controls allow users to compose and structure content entirely through the interface. Every click or keystroke that modifies the visual output generates corresponding HTML in the background.

More advanced editors extend this with features like inline image resizing, drag-and-drop media positioning, table management, and real-time collaboration. Froala, for example, provides a clean toolbar-driven interface with a source view toggle, so technical users can inspect or edit the underlying HTML directly without switching to a separate tool.

Common Users of HTML WYSIWYG Editors

WYSIWYG editors are most commonly used by content creators, copywriters, and marketing teams who are responsible for producing and publishing HTML content but aren’t expected to write or maintain markup. Business users who manage documentation, internal wikis, or landing page copy also benefit significantly. Essentially, anyone who needs to create structured HTML content without a development background is the natural audience for a WYSIWYG editor.

Understanding Code Editors

The code editor occupies the opposite end of the spectrum and serves a fundamentally different set of needs.

What Is a Code Editor?

A code editor is a text-based environment designed for writing and editing markup directly. HTML appears as raw text: tags, attributes, values, and structure are all visible and editable. There’s no automatic rendering or visual feedback; the developer writes code and views the output separately, typically in a browser preview.

Code editors range from lightweight text editors with syntax highlighting to full IDEs with linting, autocompletion, project management, and integrated version control. Their defining characteristic is that they give the user complete, unmediated access to the markup.

How Code Editors Support Content Creation

For technical users, writing HTML directly offers a level of control that no visual editor can fully replicate. Every tag, every attribute, every class name and inline style is explicit and intentional. There’s no risk of a toolbar action generating unexpected markup or wrapping content in extra elements.

This makes code editors particularly valuable for complex content structures: custom layouts, non-standard semantic markup, schema.org annotations, or content that integrates tightly with JavaScript frameworks. When the output needs to meet precise technical specifications, writing directly in code is the most reliable path to that outcome.

Common Users of Code Editors

Front-end developers are the primary users of code editors for HTML content work. Technical content teams, documentation engineers, developer advocates, or web administrators who are comfortable with markup also rely on them. In agencies or product teams, code editors are typically the tool of choice for anyone building or maintaining the templates and components into which content is placed.

Ease of Use Comparison

The usability gap between WYSIWYG and code editors is real, significant, and worth taking seriously when making tooling decisions.

The comparison table below scores both approaches across eight key dimensions. WYSIWYG editors lead on ease of use, collaboration, and speed; code editors lead on SEO control, code quality, and customisation.

Dimension HTML WYSIWYG Editor Code Editor
Ease of Use ⭐⭐⭐⭐⭐ 5/5 ⭐⭐ 2/5
Content Speed ⭐⭐⭐⭐⭐ 5/5 ⭐⭐⭐ 3/5
SEO Control ⭐⭐⭐ 3/5 ⭐⭐⭐⭐⭐ 5/5
Collaboration ⭐⭐⭐⭐⭐ 5/5 ⭐⭐ 2/5
Code Quality ⭐⭐⭐ 3/5 ⭐⭐⭐⭐⭐ 5/5
Customisation ⭐⭐⭐ 3/5 ⭐⭐⭐⭐⭐ 5/5
Onboarding ⭐⭐⭐⭐⭐ 5/5 ⭐⭐ 2/5
Scalability ⭐⭐⭐⭐ 4/5 ⭐⭐⭐ 3/5

Learning Curve for HTML WYSIWYG Editors

A well-designed WYSIWYG editor requires almost no technical onboarding. If a user can operate a word processor, they can operate a WYSIWYG editor. The toolbar makes formatting options visible and self-explanatory, and the visual output means users get immediate feedback on what their changes do.

This has a direct impact on onboarding time. A new content team member can be productive in a WYSIWYG editor within hours, not days.

Learning Curve for Code Editors

Code editors require a working knowledge of HTML and CSS at minimum. For anything beyond basic markup: custom attributes, embedded media, schema annotations- users need a solid technical foundation. This isn’t a problem for developers, but it creates a genuine barrier for anyone who wasn’t hired to write code.

The learning curve also extends to the tools themselves. IDEs with extensive configuration options, keyboard shortcuts, and extension ecosystems take time to master, even for experienced developers.

Productivity Impact

For non-technical users, WYSIWYG editors are significantly faster. Formatting that would require writing and testing multiple lines of HTML takes a single toolbar click. For developers doing complex technical work, code editors are faster because they don’t abstract away the markup; every change is direct and intentional, with no intermediary layer to navigate.

The key insight is that productivity is user-dependent: the right tool for a task is the one that matches the skill level of the person doing it.

Content Creation Speed and Efficiency

Speed matters more than it used to. Publishing cadences have accelerated, and content teams are expected to produce more, more quickly, across more channels.

Advantages of HTML WYSIWYG Workflows

WYSIWYG editors are optimised for speed at the authoring stage. Media insertion is typically drag-and-drop, formatting takes a single click, and tables, lists, and headings can be created without knowledge of the underlying markup. The right HTML editor software for content creators can therefore reduce publishing friction and deliver meaningful time savings for teams producing content frequently.

Advantages of Code-Based Workflows

Code editors shine when precision is the priority over speed. A developer who knows exactly what markup is needed can produce a complex, custom-structured HTML document quickly, and with a level of control that a WYSIWYG editor can’t match. There’s no risk of the editor second-guessing intent or wrapping content in unexpected elements.

For templates, reusable components, or content that integrates with a build process, code editors also integrate more naturally into existing development workflows.

Which Approach Saves More Time?

This depends on who’s doing the work and what they’re creating. For standard content creation: articles, landing page copy, product descriptions, documentation, WYSIWYG editors are almost always faster for non-technical users. For custom layouts, technical integrations, or content that requires precise markup, code editors save time by removing the abstraction layer.

For teams with both content creators and developers, the answer is often: use both for different parts of the workflow.

Collaboration and Team Workflows

Collaboration is where the choice of editor has the most visible impact on team dynamics.

Why Content Teams Prefer HTML WYSIWYG Editors

WYSIWYG editors democratise content creation. When anyone on the team can draft, format, and update content without needing a developer’s help, the content pipeline becomes significantly more fluid. Review and approval workflows are easier to manage when reviewers can see exactly what the published content will look like, directly in the editor.

This also reduces bottlenecks. In organisations where all content changes require a developer to touch the HTML, a single team member’s availability can block publishing. WYSIWYG editors redistribute that responsibility.

Collaboration Challenges with Code Editors

Code editors create a dependency: if the person editing the HTML needs technical knowledge that most team members don’t have, collaboration narrows to a small group. Non-technical stakeholders can’t review or edit the raw markup, which means they have to either trust the output or wait for a rendered preview to review changes.

In fast-moving content environments, this bottleneck is a real operational cost.

Hybrid Collaboration Models

The most effective modern content teams often operate with a hybrid model: a WYSIWYG editor handles the majority of day-to-day content work, while a code view or developer-accessible HTML source is available for technical users who need it. This supports both groups without forcing either into a suboptimal workflow.

Froala is built around this model; it provides a visual editing interface for content creators alongside a clean HTML source view that developers can use when they need direct markup access.

Content Quality and Consistency

Quality isn’t just about what content says; it’s about how consistently it’s structured, formatted, and presented across a growing library of pages.

Maintaining Brand Standards with HTML WYSIWYG Editors

WYSIWYG editors naturally enforce consistency by limiting the formatting options available to users. If the toolbar only offers approved heading styles and pre-defined color options, users can’t accidentally introduce off-brand formatting. This is a significant advantage for organisations managing content at scale, where consistency is difficult to enforce through manual review.

Standardised content structures also make downstream processes, template updates, content migrations, and SEO audits significantly easier to manage.

Content Flexibility with Code Editors

Code editors offer complete formatting freedom, which is both an advantage and a risk. Developers can implement custom layouts, unique content structures, and advanced styling that no WYSIWYG toolbar would expose. For one-off technical content, this flexibility is valuable.

For teams managing large content libraries, however, complete freedom can become a consistency liability. Without strong conventions and code review processes, content created in code editors can accumulate structural inconsistencies over time.

Governance and Editorial Oversight

Content governance, controlling who can change what, and ensuring that changes meet quality standards before publishing, is generally easier to implement with WYSIWYG editors. Role-based permissions, approval workflows, and content templates can constrain what individual users are able to produce, reducing the risk of errors reaching publication.

SEO and Content Optimisation Considerations

The radar chart below shows the capability profile of each editor type. WYSIWYG editors score higher on collaboration and speed; code editors score higher on SEO control and customisation. The ideal tool depends on which axes matter most for your team.

A visual diagram representing a radar chart that shows the capability profile of each editor type

Both WYSIWYG and code editors can support strong SEO, but they do so differently.

SEO Benefits of HTML WYSIWYG Editors

WYSIWYG editors make it easy for non-technical content creators to follow SEO best practices without understanding the underlying markup. Heading hierarchy (H1, H2, H3) is enforced through the toolbar. Alt text for images is prompted at the point of insertion. Meta descriptions and title tags are often managed through companion fields in the CMS.

This means SEO optimisation is accessible to everyone producing content, not just the developers responsible for the markup.

SEO Flexibility with Code Editors

Code editors give developers complete control over every SEO-relevant element: heading structure, schema.org markup, canonical tags, Open Graph metadata, structured data for rich snippets, and more. For technically complex pages, product pages with structured data, local SEO landing pages, or content that requires custom schema implementations, this level of control is difficult to replicate in a WYSIWYG environment.

Choosing the Right Tool for SEO Goals

If your SEO strategy is driven by content quantity and quality, publishing frequently, and optimising on-page elements at the authoring stage, WYSIWYG editors support this workflow well. If it’s driven by technical SEO at scale, with custom markup and structured data as a core part of the strategy, code editors give developers the precision they need.

Many organisations benefit from both: WYSIWYG for day-to-day content SEO, code editor access for technical implementation.

Integration and Scalability

Tools don’t exist in isolation; they need to connect to the platforms and systems already in use.

HTML WYSIWYG Integration Capabilities

WYSIWYG editors are designed to be embedded. They integrate with content management systems, marketing platforms, documentation tools, and custom web applications through APIs and plugins. Froala, for example, provides framework-specific integrations for React, Angular, Vue, and others, making it straightforward to add rich editing capabilities to existing applications without building from scratch.

This embeddability is a key advantage: the editor becomes part of the product rather than a separate tool that users have to switch to.

Code Editor Integration Strengths

Code editors integrate naturally into development workflows: version control systems, build tools, CI/CD pipelines, and local development environments. For content that lives inside a codebase: component documentation, static site generator content, template logic, and code editors fit more naturally into the existing technical workflow.

Scaling Across Growing Teams

WYSIWYG editors scale more easily for growing content teams because they don’t require technical skills from every new contributor. Adding ten new content creators to a WYSIWYG-based workflow is straightforward. Adding ten new contributors to a code-editor-based workflow requires either significant training investment or technical hiring.

For teams that expect their contributor base to grow and diversify, WYSIWYG editors provide a more accessible foundation.

Security and Content Management

Security is often an afterthought in tooling decisions until it becomes a problem.

Security Features in HTML WYSIWYG Editors

Modern WYSIWYG editors include content sanitisation as a core feature. User-generated or pasted content is filtered to remove potentially malicious markup (XSS payloads, injected scripts, unsafe attributes) before it’s rendered or stored. Permission management and role-based access controls allow administrators to define what individual users can modify, reducing the risk of accidental or intentional content damage.

Security Considerations for Code Editors

Direct HTML editing introduces risks that WYSIWYG editors mitigate automatically. A developer, or a user with code editor access, can introduce malicious or poorly structured markup without the validation layer that a WYSIWYG editor provides. For environments where multiple users have markup-level access, additional validation and review processes are necessary.

Managing Content Safely at Scale

As content teams grow and publishing velocity increases, the manual review processes that work at a small scale become impractical. Editors with built-in sanitisation, permission controls, and approval workflows provide a more sustainable security model than relying on individual contributors to follow coding conventions.

Cost and Resource Considerations

The total cost of a tooling decision includes more than the software license.

Training and Adoption Costs

WYSIWYG editors have dramatically lower training costs for non-technical users. A content creator can be onboarded in an afternoon. Code editors require investment in HTML/CSS training or technical hiring, which has a significantly higher cost, both in time and money.

Operational Efficiency

The ongoing operational cost of a WYSIWYG-based workflow is lower for content-heavy teams because it removes the developer dependency for routine content tasks. Every piece of content that doesn’t require a developer to publish is a small efficiency gain that compounds across hundreds of publishing cycles.

Total Cost of Ownership

WYSIWYG editors embedded in a product or CMS typically have a higher upfront integration cost but lower ongoing operational cost for non-technical teams. Code editors have a lower tool cost but a higher ongoing cost in terms of technical staffing and training requirements.

Final Recommendation for Modern Content Teams

The answer depends on who your team is and what they need to do.

You can use the flowchart below to identify the right fit based on team composition, technical requirements, and content workflow needs.

A visual diagram representing which editor is right for your team

When an HTML WYSIWYG Editor Is the Best Choice

WYSIWYG editors are the right primary tool for content-heavy organisations where non-technical contributors are responsible for producing and publishing HTML content. Marketing-led publishing workflows, documentation platforms, internal knowledge bases, and CMS-driven websites all benefit from the accessibility and speed of a WYSIWYG environment. If your team includes more content creators than developers, a WYSIWYG editor is almost certainly the better default.

When a Code Editor Makes More Sense

Code editors are the right primary tool for technically complex content work: custom web development, template creation, content that integrates with a build system, or any workflow where precise markup control is a core requirement. For developer-led projects where all contributors are comfortable with HTML, the abstraction layer of a WYSIWYG editor adds complexity rather than reducing it.

Why Many Teams Use Both

The most practical answer for most modern organisations is a hybrid approach. WYSIWYG editors handle the volume of day-to-day content work, keeping the publishing pipeline accessible to the whole team. Code editors remain available for technical users who need them for specific tasks. A well-designed editor, like Froala, supports this directly, with a rich visual interface for content creators and a clean HTML source view for developers who want to work closer to the markup.

Conclusion

The choice between an HTML WYSIWYG editor and a code editor isn’t a purely technical decision; it’s an organisational one. It reflects how your team is structured, who’s responsible for content, and what kind of publishing velocity and quality you’re aiming for.

For most modern content teams, html wysiwyg editors provide the better default: they’re faster for non-technical users, easier to collaborate with, and simpler to maintain at scale. Code editors remain indispensable for developers who need precision and control.

The most effective organisations don’t treat this as an either/or decision. They build workflows that put the right tool in front of the right person and choose editors flexible enough to serve both. See how Froala supports both workflows with a visual editing experience for content teams and clean HTML source access for developers.

Frequently Asked Questions

What is an HTML WYSIWYG editor?

An HTML WYSIWYG editor is a visual content creation tool that lets users format and compose content while automatically generating valid HTML in the background. The interface shows the content as it will appear when published, without requiring users to write or understand markup.

Are HTML WYSIWYG editors better than code editors?

It depends on who’s using them and what they’re doing. WYSIWYG editors are significantly better for non-technical users who need to create content quickly and collaboratively. Code editors are better for developers who need precise control over markup, custom implementations, or technical content that integrates with a build process.

Do content teams need coding knowledge to use HTML WYSIWYG editors?

No. WYSIWYG editors are designed to work without any coding experience. Formatting, media insertion, and content structuring are all handled through the visual interface. Technical knowledge can be useful for advanced features, but it’s not required for standard content creation tasks.

Which option is better for SEO?

Both can support strong SEO. WYSIWYG editors make it easier for content creators to follow SEO best practices at the authoring stage: heading structure, alt text, and metadata fields are all accessible through the interface. Code editors give developers more control over technical SEO elements like schema markup and custom structured data. The best choice depends on whether your SEO strategy is content-driven or developer-driven.

Can organisations use both HTML WYSIWYG and code editors?

Yes, and many do. A common model is to use a WYSIWYG editor as the primary interface for content teams while keeping HTML source access available for technical users who need it. Froala supports this directly, providing a full visual editing experience alongside a clean HTML source view.

Converting Raw HTML Into a Visual Editing Interface

Converting Raw HTML Into a Visual Editing Interface

Picture handing a non-technical marketing manager a raw block of HTML and asking them to update a landing page. The <div> tags, inline styles, and nested <span> elements are not just unfamiliar; they are a barrier. One misplaced bracket can break the entire layout. This is the everyday friction that HTML-to-WYSIWYG conversion was built to solve.

WYSIWYG (What You See Is What You Get) editors translate raw HTML markup into a visual, intuitive editing surface. Instead of wrestling with code, users click, type, and format content the same way they would in a word processor, while the editor quietly generates clean, valid HTML behind the scenes. The result is faster publishing, fewer errors, and content workflows that genuinely scale.

At Froala, this problem sits at the core of what we think about when building editing experiences. This article walks through exactly how the conversion works, what makes a WYSIWYG editor effective, and what teams across industries gain by making the switch.

Key Takeaways

  • WYSIWYG editors convert raw HTML into a visual editing surface, removing the need for users to touch code directly.
  • The conversion process involves parsing HTML, building a live DOM, and synchronising visual output with clean HTML generation.
  • Non-technical users, marketers, and enterprise teams all benefit from reduced onboarding time and faster content production.
  • Successful implementations require attention to rendering accuracy, clean HTML output, browser compatibility, and content security.
  • Modern WYSIWYG solutions integrate with CMS platforms, SaaS products, and enterprise systems to standardise content workflows at scale.

Understanding HTML to WYSIWYG Conversion

Before exploring how it works, it helps to understand what the conversion actually means and why it matters for teams that need to move fast without writing code.

What Does HTML to WYSIWYG Mean?

HTML is a markup language. Every element of a web page: a heading, a paragraph, a bold word, is expressed as a tag with attributes. It is precise, structured, and entirely text-based. For developers, that structure is intuitive. For everyone else, it is noise.

HTML to WYSIWYG conversion is the process of taking that raw markup and rendering it as a live, editable visual interface. A user clicking “Bold” in a WYSIWYG toolbar does not need to know that <strong> is being added to the source. They see bold text. They move on.

The relationship between HTML and its visual output is one-to-one in principle, but the editor’s job is to make that mapping feel invisible. A well-built editor parses incoming HTML, constructs an internal representation of the document, renders it faithfully, and then writes clean HTML back whenever the content changes.

Why Visual Editing Matters

The case for visual editing is fundamentally about access. When editing requires technical knowledge, content becomes a bottleneck. Developers get pulled into tasks they should not own. Marketers wait for tickets to be resolved. Publishing slows down.

WYSIWYG editors remove that bottleneck by lowering the skill floor for content creation. A content strategist can update a product page. A support lead can edit a knowledge base article. A brand manager can format a newsletter without filing a single request to engineering. The productivity gains compound quickly, especially in teams that publish frequently.

Beyond speed, visual editing also reduces the error rate. When users can see what they are producing in real time, formatting mistakes are caught immediately rather than discovered after publishing.

Common Use Cases

HTML-to-WYSIWYG conversion is relevant across a wide range of applications:

  • Content management systems where editorial teams publish without developer involvement.
  • Website builders that give users direct control over page layouts and copy.
  • Internal business tools such as knowledge bases, training portals, and communication platforms where non-technical users need to create and update rich content.
  • SaaS platforms that need to offer rich-text editing as a built-in feature for their own users.

HTML-to-WYSIWYG workflows can also support legacy content modernization by turning older HTML assets into editable content that teams can clean up, update, and repurpose within modern platforms.

How HTML Becomes a Visual Editing Surface

The conversion from markup to an editable surface is not a single operation; it is a pipeline with several distinct stages working together.

Visual diagram representing how HTML becomes a visual editing surface

Rendering HTML in an Editor

When HTML is loaded into a WYSIWYG editor, the first task is parsing. The editor reads the markup and constructs a Document Object Model (DOM), a structured, in-memory tree representing every element and its relationships. This is the same mechanism browsers use to render web pages.

Once the DOM is built, the editor applies styles and layouts to produce a visual representation that mirrors what users would see in a browser. The key challenge here is fidelity: the editing view and the published view should look as close to identical as possible. Discrepancies between the two: a font that looks different, a table that shifts, erode trust and lead to errors.

Modern editors handle this by running rendering logic that closely replicates browser behaviour, including support for responsive layouts, media elements, and custom CSS.

Mapping User Actions to HTML

Every interaction in a WYSIWYG editor corresponds to an HTML operation. Selecting text and clicking “Heading 2” wraps that content in <h2> tags. Inserting an image creates a properly structured <figure> element. Creating a list produces <ul> and <li> elements.

This mapping is what makes visual editing possible. The editor intercepts user intent, a keyboard shortcut, a toolbar click, a drag-and-drop operation, and translates it into the appropriate markup changes. This happens instantly and invisibly, maintaining a live connection between what users see and what the HTML output contains.

Good editors also handle edge cases: merging adjacent formatting spans, avoiding redundant tags, and keeping the output as clean as semantically possible.

Synchronising Visual and Source Views

Many editors support a dual-view mode, allowing users to toggle between the visual surface and the raw HTML source. This is valuable for developers who need to fine-tune markup directly or for content teams that occasionally need to embed custom code snippets.

Keeping these two views synchronised is technically demanding. Any change made in the source view must be immediately reflected in the visual view, and vice versa. Editors who handle this well give teams flexibility without sacrificing consistency.

Benefits of Converting HTML to WYSIWYG

The business case for WYSIWYG conversion goes beyond convenience. It reshapes how teams work, who owns content, and how quickly ideas reach an audience.

Dimension Raw HTML Editing WYSIWYG Editing
Technical skill Requires knowledge of HTML tags, attributes, and structure, developer needed No coding knowledge required, accessible to all
Editing speed Slow: every format change requires manual markup updates Fast: one click to format, instant visual feedback
Error rate High: typos in tags break layouts silently Low: what you see is what gets published
Onboarding Long ramp-up; new contributors need HTML training before editing independently Minutes, not weeks, familiar interface similar to a word processor
Collaboration Difficult: only technical teammates can meaningfully review the source Seamless: any stakeholder can read, edit, and review content
Output control Full control: the developer can write any markup. Maximum flexibility Controlled by editor rules: clean, consistent output within defined constraints
Best for Complex custom layouts, one-off components, and developer-owned content Content teams, marketers, documentation, and high-volume publishing workflows

Improved User Experience

The most immediate benefit is an editing experience that does not require a manual. Non-technical users can start creating content on day one without training sessions on HTML syntax. Formatting options are visible and labeled. The result is an interface that feels familiar rather than foreign.

This matters especially as organisations grow and onboard new contributors. Every hour spent teaching someone HTML is an hour not spent on content. A visual editor eliminates that cost entirely.

Increased Content Production Efficiency

When content teams can work independently, publishing pipelines accelerate. There is no handoff to a developer for formatting fixes. No waiting on a code review to push a copy change. Marketing can iterate on campaign pages in real time. Support teams can update documentation the moment a product changes.

This speed compounds over time. Teams that publish faster learn faster; they can test messaging, update information, and respond to audience feedback without engineering delays slowing the feedback loop.

Better Collaboration Across Teams

WYSIWYG editors also make collaboration more natural. When everyone on a team can view and edit content in the same visual format, reviews are faster, and feedback is more specific. A content strategist can comment on a layout choice. A designer can refine a heading. An editor can approve changes, all without anyone needing to interpret code.

Shared editing access, combined with role-based permissions, enables editorial governance at scale without creating bottlenecks.

Key Components of an HTML to WYSIWYG Editor

Not all WYSIWYG editors are built equally. Understanding what separates a capable editor from a basic one helps teams make better implementation decisions.

Content Rendering Engine

The rendering engine is the foundation of any WYSIWYG editor. It is responsible for parsing incoming HTML, building the document model, and displaying the visual editing surface accurately. A strong rendering engine handles complex layouts, embedded media, custom components, and dynamic content without distorting the output.

Consistency between the editing view and the published view is the primary benchmark here. When these diverge, even slightly, users lose confidence in what they are producing.

Editing Toolbar and Controls

The toolbar is the primary interface through which users interact with content. Effective toolbars offer the most common formatting options (headings, bold, italic, lists, alignment) without overwhelming users with options they rarely use.

Beyond basic formatting, mature editors provide media insertion tools, table management, link handling, and support for embedded content like iframes or custom HTML blocks. The best implementations allow toolbar customisation so that teams can surface only the controls relevant to their workflow.

Content Output Management

The final stage of the pipeline is what the editor produces. Clean, semantic HTML output is not a nice-to-have; it is essential for long-term content quality, SEO performance, and accessibility compliance.

Editors that generate bloated or non-semantic markup create downstream problems: content that is harder to restyle, index, or migrate. Output management also includes how content is stored (in a database, as flat files, via an API) and how it flows through publishing and export workflows.

Challenges in HTML to WYSIWYG Conversion

Understanding the benefits is important, but so is understanding where the hard problems lie. Teams that plan for these challenges build better implementations.

Handling Complex HTML Structures

Standard paragraphs and headings are straightforward to render visually. Custom layouts, nested components, embedded iframes, and advanced CSS structures are not. When HTML contains elements the editor does not know how to represent visually, it must make a decision: render a fallback, expose the raw markup, or block the import entirely.

Teams migrating existing content into a WYSIWYG editor often encounter this challenge. Legacy markup written for a previous CMS may not map cleanly to the new editor’s content model. Preprocessing pipelines that clean and normalise HTML before import can reduce friction significantly.

Preserving Clean HTML Output

Every WYSIWYG editor has a tendency to add markup of its own. One of the central challenges of preserving HTML during WYSIWYG conversion is balancing visual fidelity with clean semantic markup while avoiding unnecessary styles, wrappers, or changes to valid HTML. Inline styles, wrapper <div> elements, non-semantic spans- these accumulate over time and make content harder to maintain. An editor who produces clean, minimal HTML from day one is far easier to live with than one who gradually accumulates formatting debt.

Semantic structure matters too. Using <h2> for a subheading rather than <p style=”font-size:1.5em”> has real consequences for accessibility tools, search engines, and future content transformations.

Managing Browser Compatibility

WYSIWYG editors run in browsers, and browsers do not all behave identically. An editor that works perfectly in Chrome may exhibit layout differences in Safari or Firefox. Responsive editing experiences, where the editing surface adapts to different screen sizes, add another layer of complexity.

Teams deploying editors across diverse user environments need to test rendering fidelity across browsers and devices before rolling out to production.

HTML to WYSIWYG in Content Management Systems

CMS integrations represent some of the most mature and high-impact applications of WYSIWYG technology. When the editor is embedded at the heart of a content platform, the whole organisation benefits.

Simplifying Content Publishing

In a traditional CMS workflow, non-technical contributors often depend on developers or template builders to publish content accurately. WYSIWYG editors shift that ownership. Writers and editors can build pages, format articles, and publish updates without waiting for technical support, reducing bottlenecks and accelerating time-to-publish significantly.

This is especially valuable for organisations with high content velocity: news publishers, e-commerce teams, and product documentation teams, where new content is created daily.

Supporting Marketing Teams

Marketing teams benefit particularly from WYSIWYG-integrated CMS platforms. Campaign landing pages, promotional banners, email templates, and blog posts can all be created and updated without touching code. This enables faster iteration on messaging, easier A/B testing of copy, and quicker responses to campaign performance data.

When marketing teams are not waiting on engineering, they can work at the speed of the market.

Enhancing Enterprise Content Operations

At enterprise scale, content management is a governance challenge as much as a productivity one. WYSIWYG editors support standardization by constraining what users can do within defined content models, preventing off-brand formatting, enforcing structural templates, and routing content through approval workflows before it reaches production.

Large organizations managing thousands of content assets benefit from this combination of accessibility and control.

Integration Considerations for Developers

Implementing a WYSIWYG editor well requires thinking beyond the editing surface itself. How the editor connects to existing systems matters as much as what it can do in isolation.

Connecting Editors to Existing Applications

WYSIWYG editors need to fit into the technical ecosystems they serve. That means clean embedding APIs, compatibility with legacy systems, and clear interfaces for reading and writing content. Editors that expose their core functionality through well-documented JavaScript APIs are significantly easier to integrate than those requiring heavy configuration or framework-specific adapters.

Developers should evaluate editors based on how they handle content initialisation (loading existing HTML), how they expose content changes (events, callbacks, direct DOM access), and how they manage the content lifecycle from creation to storage to rendering.

API and Customisation Requirements

Most production implementations need more than a default editor configuration. Custom toolbar layouts, extended formatting options, third-party plugin integration, and custom content blocks all require an editor with a flexible extension model.

Teams building on top of a WYSIWYG editor should look for clear documentation on plugin development, event hooks for custom behaviours, and documented APIs for programmatic content manipulation.

Performance Optimisation Strategies

Editors running inside complex web applications need to be lightweight and responsive. Large documents, real-time collaborative editing, and frequent autosaving can all introduce performance overhead if the editor is not well-optimised.

Key considerations include lazy loading of editor assets, efficient DOM update strategies, and throttling of content serialisation to avoid blocking the main thread on large documents.

Security and Content Integrity

Content entered through a visual editor is user-generated content, and user-generated content introduces security considerations that cannot be ignored.

Content Sanitization

WYSIWYG editors must sanitise HTML before storing or rendering it. Without sanitisation, a malicious user can inject script tags, event handlers, or external resource references that execute arbitrary code, a classic XSS vector. Robust editors sanitise on both input and output, stripping elements and attributes that fall outside a defined allowlist.

Teams should review their editor’s sanitization defaults and extend them to match their specific content security requirements. Never assume the default configuration is sufficient for production use.

User Permissions and Access Control

Not all users should have the same editing capabilities. A junior contributor might need to write copy but not insert raw HTML or embed iframes. An editor might need publishing rights but not the ability to modify site-wide templates. Role-based access controls at the editor level, not just the application level, ensure that the editing surface matches each user’s responsibilities.

Maintaining Content Consistency

Governance tools built on top of WYSIWYG editors: content templates, style guides enforced programmatically, approval workflows, help maintain quality at scale. When anyone can edit, it takes deliberate system design to ensure that not everything gets published as-is.

Best Practices for HTML to WYSIWYG Implementations

Drawing from real-world deployments, a few principles consistently separate successful WYSIWYG integrations from frustrating ones.

Prioritise Ease of Use

The whole point of a WYSIWYG editor is to reduce friction. A toolbar overloaded with rarely-used features, a slow rendering engine, or a confusing layout undermines that goal. Start with minimal, opinionated defaults and add complexity only when users request it. The best editing interfaces get out of the way.

Maintain Clean and Semantic HTML

Treat clean output as a first-class requirement from day one. Establish rules for what the editor is allowed to generate. Test output against semantic HTML standards. Review content periodically for formatting drift. The cleaner the output stays, the easier the content is to maintain, migrate, and optimise over time.

Plan for Scalability

Content needs to grow. Teams expand. New content types emerge. Editors chosen for their simplicity on day one must be capable of growing with the organisation. Evaluate the editor’s roadmap, its community, its extensibility model, and its track record with enterprise-scale deployments before committing to an implementation.

Common Industries Using HTML to WYSIWYG Solutions

The demand for visual editing spans virtually every sector, though a few industries have particularly strong adoption.

Media and Publishing

News organisations and digital publishers often manage hundreds of articles per day across large editorial teams. WYSIWYG editors allow journalists, editors, and producers to work in the same environment without format inconsistencies. Multi-author workflows, inline media handling, and clean semantic output are essential in these environments.

Software and SaaS Platforms

SaaS companies frequently embed WYSIWYG editors as features within their own products. Knowledge base tools, documentation systems, onboarding flows, and user-generated content features all depend on a stable, embeddable editing experience. For these teams, the editor is an infrastructure component; it must be reliable, customizable, and well-supported.

Corporate and Enterprise Organizations

Large enterprises use WYSIWYG-enabled platforms for internal communications, training materials, HR documentation, and customer-facing content initiatives. In these environments, governance and consistency matter as much as speed. WYSIWYG editors, combined with content approval workflows and role-based access, enable broad participation without sacrificing quality or compliance.

Conclusion

The gap between raw HTML and a usable editing surface is not just a technical gap; it is an organisational one. Every time a non-technical user has to request developer help to make a content change, there is a cost: in time, in attention, and in the agility of the team. WYSIWYG editors close that gap by making content creation genuinely accessible.

Done well, the conversion from HTML to a visual editing surface removes barriers without removing control. Content teams move faster. Developers stay focused on product work. Published output stays clean, consistent, and semantically sound.

Froala has spent years building a WYSIWYG editor designed to handle this balance, offering the flexibility developers need and the simplicity content teams expect. If you are evaluating how to bring visual editing into your platform or workflow, Froala’s editor is a strong place to start.

Frequently Asked Questions

What does HTML to WYSIWYG mean?

It refers to the process of converting raw HTML markup into a visual editing interface where users can create and modify content without directly touching code. The editor handles all markup generation automatically.

Why is HTML to WYSIWYG conversion important?

It makes content creation accessible to non-technical users, accelerates publishing workflows, and reduces the dependency on developers for routine content updates.

Can a WYSIWYG editor preserve existing HTML formatting?

Most modern WYSIWYG editors are designed to import and preserve existing HTML structure and formatting while allowing users to edit visually. The fidelity depends on the editor’s rendering engine and how well it handles the specific markup in question.

What are the biggest challenges in HTML to WYSIWYG implementations?

Managing complex or non-standard HTML structures, maintaining clean semantic output over time, ensuring consistent rendering across browsers, and properly sanitising user-generated content are the most common challenges.

Who benefits from HTML to WYSIWYG editors?

Content creators, marketers, technical writers, developers building content features, publishers, and enterprise teams managing large-scale content operations all benefit from the accessibility and efficiency that visual editing provides.

HTML Editor Linux Cross-Platform Options That Work

HTML Editor Linux Cross-Platform Options That Work

Linux has long been the operating system of choice for developers who value control, performance, and flexibility. But when it comes to HTML editing, Linux users often face a quieter challenge: finding tools that don’t just work on Linux but work alongside everyone else on the team using Windows or macOS.

Whether you’re a solo developer, part of a distributed content team, or an engineering lead building a product that runs on multiple platforms, the HTML editor Linux question comes down to more than syntax highlighting. It’s about whether your editor can keep up with the realities of modern multi-OS workflows without creating friction.

This guide covers what to look for, how the major categories compare, and how to choose the right option for your specific context, with a plain look at trade-offs across browser-based, downloadable, and embedded editors.

Key Takeaways

  • Cross-platform compatibility is a practical necessity for most modern development and content teams, not a nice-to-have.
  • Browser-based editors eliminate OS restrictions entirely and are ideal for collaborative, distributed environments.
  • Downloadable and Electron-based editors work well for offline or security-sensitive workflows where local installation matters.
  • WYSIWYG editors like Froala offer consistent, rich editing experiences that embed natively into both Linux and cross-platform applications.
  • The right choice depends on team size, technical requirements, and whether you’re building for developers, content creators, or enterprise users.

Why Linux Users Need Cross-Platform HTML Editors

Linux holds a strong position in developer tooling, server environments, and technical teams, but it’s rarely the only OS in the room. As soon as collaboration enters the picture, platform-specific editors become a liability.

The Rise of Multi-OS Development Teams

Today’s development teams are rarely homogeneous. A typical product team might include frontend developers on Ubuntu, designers on macOS, and project stakeholders on Windows. Shared workflows across these environments create real pressure on tooling: if your HTML editor produces output that behaves differently depending on what OS it was authored on, that’s a problem that compounds fast.

Distributed and hybrid teams amplify this further. When people are editing the same templates or content from different machines, inconsistency in how markup is generated, pasted, or formatted can introduce bugs that are genuinely difficult to trace back to their source.

Challenges of Platform-Specific Tools

Platform-specific HTML editors tend to create onboarding friction. When new team members join and happen to use a different OS, they need a different tool, which often means a different workflow, different keyboard shortcuts, and a different mental model for the same task.

Compatibility limitations are the other side of this. Files created with tools that rely on OS-level integrations can behave unexpectedly when opened elsewhere. This is especially common with editors that depend on native clipboard behaviour, font rendering, or file system access patterns specific to one OS.

Benefits of Cross-Platform Solutions

A cross-platform HTML editor for Linux should maintain consistent behaviour across Linux, Windows, and macOS. This supports easier onboarding, smoother collaboration, and fewer compatibility problems when team members work from different operating systems.

What to Look for in an HTML Editor for Linux

Not all cross-platform editors are created equal. Before committing to one, it’s worth thinking carefully about the specific properties that matter most for your workflow. Choosing a Linux-compatible HTML editor involves evaluating essential features such as ease of use, performance, framework integration, and visual editing capabilities to find a solution that meets your development needs.

Cross-Platform Compatibility

The baseline requirement is genuine parity: the editor should behave the same on Linux, Windows, and macOS, not just install on all three. Watch for editors that technically support multiple operating systems but have known gaps in functionality on Linux (missing plugins, slower performance, or UI rendering issues).

Browser-based editors sidestep this problem almost entirely, since they run in a standards-compliant runtime regardless of the underlying OS.

Ease of Installation and Deployment

Installation complexity matters more than it might seem, especially for teams. A browser-based editor embedded in a product or CMS requires no local installation at all; users just open a browser. A downloadable application needs to be installed, updated, and maintained across every machine it runs on.

For Linux specifically, consider whether the editor is distributed as a .deb, .rpm, AppImage, or via a package manager like Snap or Flatpak. Electron-based apps often come as AppImages, which work across distributions without dependencies, a practical advantage.

Performance and Reliability

HTML editors that load slowly, freeze on large documents, or break under common browser extensions create subtle productivity costs that add up over time. For Linux environments especially, where you might be running on older hardware or a lightweight distribution, resource efficiency is worth evaluating before committing to a tool.

Browser-Based HTML Editors for Linux

Browser-based editors are arguably the most natural fit for Linux environments. They don’t require OS-level installation, they render consistently across machines, and they can be embedded directly into web applications and content platforms.

Editor Type Linux macOS Windows Mobile Browser
Browser-Based Editor Supported Supported Supported Supported
Electron / Cross-Platform Supported Supported Supported Partial
Native Linux App Supported Not Supported Not Supported Not Supported
WYSIWYG (Embedded) Supported Supported Supported Supported

Advantages of Browser-Based Editors

The most significant advantage is freedom from OS constraints. A browser-based editor works anywhere a modern browser runs, which is virtually everywhere. Updates are handled server-side, so there’s no version drift between team members. A content editor in Berlin using Firefox on Ubuntu and a developer in Singapore using Chrome on macOS are both running the same code.

For teams building web products, this also means you can embed the editor directly into your application rather than asking users to install something separate.

Common Use Cases

Browser-based HTML editors are well-suited for content management systems, collaborative editing environments, and any product where the editing experience is part of the user-facing interface. They’re also a strong choice for remote and hybrid teams where onboarding needs to be fast and consistent across machines.

Editors like Froala fall into this category, designed to be embedded in web applications and run natively in the browser, with full feature parity regardless of the underlying operating system. For a practical example of Linux web development with Froala Editor, explore our step-by-step guide to integrating Froala with React and building a cross-platform WYSIWYG HTML editor.

Potential Limitations

The main trade-off is internet dependency. Browser-based editors generally require a network connection to function, which can be a constraint for workflows that involve working offline or in low-connectivity environments. Data governance is also worth considering, if your content is sensitive, you’ll want to understand how data flows between the browser, your server, and any third-party services the editor relies on.

Downloadable HTML Editors That Support Linux

For workflows where local installation is preferable or required, downloadable editors remain a practical and often powerful option.

Native Linux Applications

Native Linux applications have the advantage of direct file system access, offline capability, and tighter integration with the local environment. Tools like VS Code (with its HTML editing extensions), Gedit, and Kate offer solid text-based HTML editing with syntax support, but they’re primarily code editors; they don’t offer rich-text or WYSIWYG HTML editing out of the box.

For developers who are comfortable writing raw markup, native editors are fast and efficient. For content teams that need a visual editing layer, they’re a harder sell.

Electron and Cross-Platform Frameworks

Electron-based editors, built on the same runtime as VS Code and many other modern desktop apps, offer a middle path. They install like a native application, work offline, and provide a consistent experience across Linux, Windows, and macOS because they run the same JavaScript environment underneath.

The trade-off is resource usage. Electron apps run a full Chromium instance, which means higher memory consumption compared to genuinely native applications. On constrained hardware, this is worth factoring in.

When Downloadable Editors Make Sense

Downloadable editors are the right choice when offline editing is a hard requirement, when the deployment environment has strict controls over what can run in a browser, or when teams are building internal tools where the editor needs to integrate with local file systems and development workflows.

Features That Matter Most for HTML Editing

Choosing an editor is partly about format and deployment, and partly about what the editor actually does when you’re using it. Here’s what to evaluate.

Syntax and Code Editing Tools

For developers writing or maintaining HTML directly, syntax highlighting, auto-completion, bracket matching, and code folding are the baseline. Beyond that, look for support for embedded CSS and JavaScript within HTML documents, linting or validation as you type, and the ability to handle large files without degrading performance.

Visual Editing Capabilities

WYSIWYG (What You See Is What You Get) editing matters enormously for non-technical users, and increasingly for developers who want a faster content authoring experience. A good WYSIWYG layer lets users format content without touching markup, while generating clean, valid HTML underneath.

Froala is built around this principle: it provides a rich visual editing interface while producing lean, standards-compliant HTML output. This is particularly useful when the editor is embedded in a product used by both technical and non-technical team members.

Preview and Validation Tools

Real-time preview, seeing your HTML rendered as it would appear in a browser, reduces the feedback loop between writing and reviewing. Validation tools that flag malformed markup, accessibility issues, or deprecated tags add another layer of quality assurance without requiring a separate linting step.

Comparing HTML Editor Linux Options by Use Case

The right editor often depends less on technical specifications than on who will be using it and how. Developers comparing different Linux HTML editor options should also consider installation compatibility, platform support, and whether the editor fits their preferred Linux workflow.

Feature priority scores below reflect how web developers, content teams, and enterprise organisations tend to weigh different editor capabilities; the gaps are significant.

Comparing HTML Editor Linux Options by Use Case

For Web Developers

Developers typically want code-centric editors with strong syntax support, integration with version control workflows, and the ability to embed the editor into larger development toolchains. Performance on Linux and support for local development environments (running on localhost, integrating with build tools) are both important.

Cross-platform compatibility matters here primarily because development teams often span multiple operating systems, and the editor needs to behave consistently when multiple developers are working on the same codebase.

For Content Teams

Content teams prioritise the opposite end of the spectrum: visual editing, ease of use for non-technical users, and seamless integration with publishing workflows. A content editor who needs to update a landing page shouldn’t have to care about the markup generated underneath; they should be able to focus on the content itself.

For this use case, browser-based WYSIWYG editors are often the strongest fit. They require no installation, can be embedded directly into the CMS or platform the content team already uses, and generate consistent HTML output regardless of which team member created it.

For Enterprise Organisations

Enterprise environments add a layer of requirements that go beyond features: security controls, compliance with data governance standards, role-based access, audit logging, and integration with identity management systems. Scalability is also a consideration; an editor that works well for a team of five needs to keep working when that team grows to fifty.

For enterprise deployments on Linux, the key questions are whether the editor can be self-hosted, how access permissions are managed, and whether it integrates with existing infrastructure.

Integration Considerations for Cross-Platform Teams

An editor doesn’t exist in isolation; it needs to connect to the systems around it.

Connecting with Existing Systems

Most content workflows involve a CMS, a documentation platform, or a custom internal tool. The editor you choose needs to integrate cleanly with whatever is already in place, ideally through an API or a supported plugin architecture. Froala, for instance, provides an API that supports integration into custom applications and existing platforms, which is useful when the editor needs to slot into a larger product rather than replace it.

Workflow Automation Opportunities

Cross-platform editors open up automation possibilities that platform-specific tools don’t. If the editor runs in a browser environment, it can connect to content pipelines, trigger publishing workflows, or integrate with third-party services without requiring custom OS-level integrations.

Collaboration Across Operating Systems

For distributed teams, shared editing environments need to handle concurrent access gracefully, ideally with real-time collaboration features, version history, or, at minimum, consistent output across operating systems. Editors that produce different HTML depending on the platform make downstream automation harder and create inconsistencies that are difficult to debug.

Security and Compliance Considerations

Security often determines which editors are even on the table for enterprise environments.

Protecting Content and Data

Content edited in an HTML editor may include sensitive information: internal documentation, customer-facing copy, product data. Editors that process content server-side or route data through third-party infrastructure require careful evaluation. For high-sensitivity environments, self-hosted or embedded editors that keep data within your own infrastructure are typically the safer choice.

Managing Permissions

Role-based access: controlling which users can edit which content, trigger publishing, or access administrative settings is a baseline requirement for teams of any meaningful size. An editor who doesn’t support granular permissions creates audit risks and complicates compliance with regulatory standards.

Compliance Requirements

Depending on your industry, content tools may need to support audit logging, data residency requirements, or specific encryption standards. These requirements are easier to meet when the editor is embedded in your own application rather than depending on a third-party SaaS platform.

Performance Factors for Linux Environments

Performance considerations on Linux are sometimes different from other platforms, particularly for teams running on older hardware or lightweight distributions.

Resource Utilisation

Browser-based editors benefit from browser-level optimisations and generally don’t add significant resource overhead beyond what the browser itself consumes. Electron-based desktop editors are heavier, typically consuming more memory than native applications. Native Linux editors are the most efficient but often the most limited in features.

For teams running Linux on constrained hardware: development machines, embedded environments, or older laptops, the resource footprint of the editor is a real practical consideration.

Stability and Reliability

Long-running editing sessions and large documents can expose stability issues that don’t show up in short tests. Before committing to an editor, test under realistic conditions: large file sizes, multiple open tabs, and extended use.

Scaling Across Teams

Consider how updates are distributed across team members, whether there’s a central configuration that applies globally, and how the editor performs when multiple users are working simultaneously.

How to Choose the Right HTML Editor for Linux

With the landscape mapped out, here’s a framework for making the decision.

Visual diagram representing how to choose the right HTML editor for Linux

The flowchart above walks through the key decision points, from solo vs. team use to offline requirements and multi-OS environments.

Define Your Primary Use Case

Start with the clearest constraint: are you primarily a developer writing code, or a content creator who needs a visual interface? Is this for individual use or a team? Are there hard requirements around offline access or local installation? Developers comparing available options can also review this guide on choosing a WYSIWYG HTML editor for Linux to better understand the factors that affect editor selection.

These answers narrow the field quickly. Developer-focused workflows with offline requirements point toward native Linux or Electron-based editors. Content teams with collaborative needs point toward browser-based WYSIWYG editors. Teams building products that embed an editor into a larger application point toward embeddable libraries.

Evaluate Platform Requirements

Once you have a category, check the specifics: Does the editor run on your specific Linux distribution without extra configuration? Does it integrate with your existing CMS or infrastructure? Are there known issues with your browser, package manager, or system configuration?

For teams, the multi-OS question is important: does the editor behave identically on Linux, Windows, and macOS, or are there known behavioural differences that could affect output quality or collaboration?

Consider Long-Term Growth

The editor you choose today needs to handle the workflow you’ll have in two years. For growing teams, this means evaluating whether the tool scales in terms of concurrent users, content volume, and feature requirements. It also means considering vendor stability and the quality of ongoing maintenance and support.

Conclusion

There’s no single answer to the HTML editor Linux question, but there is a clear framework for finding the right one. The most important factor for most teams isn’t the feature list: it’s whether the editor creates or removes friction when people with different operating systems, technical backgrounds, and workflow needs are working together on the same HTML content.

Browser-based editors offer the broadest compatibility and the smoothest experience for collaborative teams. Downloadable editors provide offline access and tighter system integration for development-focused workflows. Embeddable WYSIWYG editors like Froala sit at the intersection; they can be integrated into web applications, run in any browser on any OS, and serve both developers and content teams without requiring platform-specific tooling.

The best approach is to start with your constraints, test against realistic workflows, and choose the tool that creates the least overhead for the people who’ll be using it every day. Explore Froala if you need a flexible, cross-platform HTML editor that can support Linux users, mixed-OS teams, and embedded product workflows from one editing experience.

Frequently Asked Questions

What is the best HTML editor Linux users can choose?

There’s no single best option; it depends on the workflow. For developers working locally, a native application or an Electron-based editor may be most suitable. For teams building web products or collaborative content environments, a browser-based WYSIWYG editor that runs cross-platform is typically the stronger choice. Cross-platform compatibility should be a key evaluation criterion regardless of team size.

Are browser-based HTML editors suitable for Linux?

Yes. Browser-based editors are among the most practical options for Linux users precisely because they’re OS-agnostic; they run the same way in any modern browser regardless of the underlying platform. This makes them especially well-suited for teams with mixed operating systems.

Can downloadable HTML editors run on multiple operating systems?

Many can. Electron-based editors in particular are designed to run on Linux, Windows, and macOS using the same codebase. Native Linux applications, by contrast, are platform-specific; they provide a better local experience on Linux but don’t transfer to other operating systems.

What features should I prioritise in an HTML editor for Linux?

The most important features depend on your use case, but cross-platform compatibility, performance, WYSIWYG editing capabilities, integration support, and security controls are the five most commonly cited priorities across developer, content, and enterprise contexts.

Is a cross-platform HTML editor better for teams?

Generally, yes. Tools that behave consistently across operating systems reduce onboarding friction, prevent output inconsistencies between team members, and support automation workflows that depend on predictable HTML output. For teams that span multiple platforms, cross-platform compatibility isn’t optional; it’s a baseline requirement.

 

How WYSIWYG editors work, with real-world examples

What Is a WYSIWYG Editor? A Plain-English Guide With Examples

Imagine writing a blog post and seeing exactly how it will look to readers: headings bold, images aligned, bullet points clean, all before you hit publish. That is precisely what a WYSIWYG editor makes possible.

WYSIWYG (pronounced wiz-ee-wig) stands for What You See Is What You Get. It is a type of content editor that lets you create and format text visually, without touching a single line of HTML. What you see on screen while editing is what your audience will see when the content goes live.

This approach to editing has transformed how people create content online. Developers no longer need to write every article. Marketing teams can update landing pages without waiting on engineering. Bloggers can style a post the way they want it, instantly. WYSIWYG editors have made content creation genuinely accessible, and that is why they are embedded in nearly every CMS, business application, and content platform you encounter today.

In this guide, we will walk through what a WYSIWYG editor is, how it works under the hood, what features to look for, and where these editors show up in the real world.

Key Takeaways

  • A WYSIWYG editor shows you a live visual preview of your content as you create it, no HTML required.
  • It generates clean underlying markup automatically, bridging the gap between design and code.
  • Common features include text formatting, media management, tables, hyperlinks, and styling controls.
  • Businesses use WYSIWYG editors to speed up publishing, reduce dependency on developers, and maintain brand consistency.
  • Not all editors are alike; choosing the right one depends on your team’s needs, technical environment, and growth plans.

Understanding the Meaning of WYSIWYG

Before diving into features and use cases, it helps to understand what the term actually means and why it matters.

What Does “What You See Is What You Get” Mean?

“What You See Is What You Get” is a design philosophy rooted in one idea: the editing experience should mirror the final output. When you bold a word in the editor, it appears bold. When you insert an image, it appears where you placed it. There is no guesswork, no mental translation between code and layout.

This stands in contrast to writing raw HTML, where you might type <strong>important</strong> and only see the styled result after saving and previewing in a browser. WYSIWYG removes that gap entirely.

The term originated in early word processing software during the 1970s and ’80s, but it found its most powerful expression in web content tools, where the distance between “writing” and “publishing” used to require a developer in the middle.

How a WYSIWYG Editor Works

At its core, a WYSIWYG editor runs a visual editing layer on top of an HTML engine. Here is what happens when you use one:

  1. You interact visually: Clicking toolbar buttons, selecting text, dragging images.
  2. The editor generates HTML: Every action you take is translated into valid markup behind the scenes.
  3. Changes appear in real time: The canvas updates instantly, so there is never a lag between what you do and what you see.

This real-time feedback loop is what makes WYSIWYG editing feel intuitive. You are essentially designing a page the way you would in a word processor, while the editor handles the technical output for you.

Why WYSIWYG Editors Were Created

The original problem was simple: creating web content required knowing HTML. That excluded most writers, marketers, and business users from participating directly in the publishing process. WYSIWYG editors were built to solve that, to lower the barrier of entry so anyone could produce well-formatted, publish-ready content without a technical background.

The goal was never to eliminate developers. It was to give non-technical users a way to be productive on their own.

The Difference Between WYSIWYG Editors and HTML Editors

Understanding this distinction helps you figure out which tool belongs in which workflow.

Visual Editing vs Manual Coding

A WYSIWYG editor and an HTML editor solve the same problem from opposite directions. With a WYSIWYG editor, you work visually; you see the formatted output as you type and apply styles through a toolbar. The HTML is generated for you. With an HTML editor, you write the markup directly and see the styled result only after rendering.

WYSIWYG Editor HTML Editor
User Content creators, marketers Developers, engineers
Input Visual clicks and formatting Raw HTML/CSS code
Output preview Real-time After rendering
Learning curve Low High
Customization Medium–High (with good tooling) Full control

 

Both are valid; the right choice depends on who is doing the editing and what they need to produce.

Learning Curve Comparison

A WYSIWYG editor requires almost no onboarding for someone who has used a word processor. The controls are familiar: a toolbar with bold, italic, lists, and image upload. Most people can start creating polished content within minutes.

An HTML editor, by contrast, requires a working knowledge of HTML and often CSS. The upside is precision: a developer can build exactly the structure they want. But for teams where content comes from writers and marketers, that trade-off rarely makes sense.

When Manual HTML Editing Makes Sense

There are scenarios where direct HTML editing remains the better choice: building highly custom page structures, embedding third-party widgets with specific markup requirements, or working inside a developer-focused CI/CD pipeline. For these cases, the constraints of a visual interface can actually slow things down.

The best modern editors, including Froala, bridge this gap by offering both modes, letting users toggle between a visual canvas and a source code view when needed.

Common Features of a WYSIWYG Editor

The quality of a WYSIWYG editor comes down to its feature set. Here is what the most capable editors typically offer.

Text Formatting Tools

This is the baseline. Any WYSIWYG editor worth using will include:

  • Inline styles: Bold, italic, underline, strikethrough, and text color.
  • Block-level formatting: Headings (H1–H6), paragraphs, blockquotes, and preformatted text.
  • Lists and alignment: Ordered lists, unordered lists, and alignment controls (left, center, right, justify).

Clean, predictable text formatting is the foundation on which everything else is built.

Media Management Features

Content without media is rare. Strong WYSIWYG editors support:

  • Image insertion: Upload directly, drag-and-drop, or reference from a URL. Good editors also handle resizing and alt-text within the interface.
  • Video embedding: Paste a YouTube or Vimeo URL and the editor handles the embed code automatically.
  • File attachments: Link to PDFs or documents inline without leaving the editor.

Media handling is often where the gap between a basic and a professional editor becomes most visible.

Advanced Editing Functions

Beyond text and media, more capable editors include:

  • Tables: Insert, resize, merge cells, and style rows, critical for structured data or documentation.
  • Hyperlinks: Add, edit, and manage links, including target behaviour (open in new tab) and link titles.
  • Content styling controls: Custom CSS classes, spacing adjustments, and block-level wrappers that align content with a brand’s design system.

These advanced features are what distinguish editors built for serious content operations from lightweight tools designed for basic input.

Benefits of Using a WYSIWYG Editor

Features matter, but the real argument for WYSIWYG editing is what it does for teams day to day.

Faster Content Creation

Writing HTML by hand takes time, time that compounds across a team publishing dozens of articles or pages a month. A WYSIWYG editor eliminates that overhead. Writers can focus on the content itself rather than the markup wrapping it. Publishing cycles that once required developer sign-off can be handled independently by the content team.

For organisations moving at speed, this efficiency is not a small thing. It is a structural advantage.

Improved User Experience

There is an onboarding cost to every tool. WYSIWYG editors dramatically lower that cost. A new hire on a marketing team can contribute content on day one, without needing to learn the company’s HTML conventions or ask a developer to review their markup.

This accessibility also reduces friction during content review cycles. Stakeholders can make minor edits directly rather than filing tickets for changes like fixing a typo or adjusting a heading level.

Consistent Content Formatting

Without guardrails, different team members produce content that looks and feels different. One person uses three heading levels. Another forgets to add alt text. A third uses inline styles that conflict with the site’s CSS.

A well-configured WYSIWYG editor enforces consistency by design. The toolbar only offers approved styles. Content is sanitised to remove rogue markup. The result is a more uniform experience across every page, which matters for both brand integrity and SEO.

Real-World Examples of WYSIWYG Editors

It is worth grounding this in where you actually encounter these tools.

Content Management Systems

The most common home for a WYSIWYG editor is a CMS. WordPress’s block editor, Contentful’s rich text field, and most other modern CMS platforms use a WYSIWYG interface to let writers and editors work without touching code. Blog publishing, website content updates, and press release pages all typically run through this layer.

Business Applications

Beyond marketing, WYSIWYG editors show up inside internal tools: knowledge bases (like Notion or Confluence), customer support platforms, and documentation systems. Anywhere a team needs to write and format content as part of a workflow, rather than as the end product itself, an embedded editor plays a critical role.

Website and Application Builders

Page builders like Webflow, Squarespace, and countless SaaS tools include WYSIWYG functionality to let non-developers create and update visual content. User-generated content platforms: review sites, community forums, and job boards also rely on embedded editors to let users contribute rich, formatted content safely.

Who Uses WYSIWYG Editors?

The short answer: almost everyone involved in creating digital content. But the use cases vary meaningfully by role.

Visual diagram representing who uses WYSIWYG editors

Content Creators and Bloggers

For individual writers and bloggers, a WYSIWYG editor is the primary workspace. It removes every technical obstacle between an idea and a published post. Writers can focus entirely on quality: structure, voice, storytelling, rather than HTML syntax.

Marketing Teams

Marketing teams live and die by speed. Campaign pages, email content, landing pages, and blog posts all need to move quickly from draft to live. A WYSIWYG editor that integrates cleanly with a CMS or marketing platform means fewer bottlenecks and more control for the people closest to the content.

Developers and Product Teams

Developers use WYSIWYG editors less for their own writing and more as a component they integrate into the products they build. If you are building a SaaS platform, a documentation tool, or any application where users need to produce formatted content, a reliable, embeddable editor is a core infrastructure choice. Froala, for example, is specifically designed to be lightweight, fast to integrate, and flexible enough for a wide range of application contexts. Developers working in Linux environments can also review how to use the Froala WYSIWYG HTML Editor on Linux for setup and integration guidance.

How WYSIWYG Editors Help Businesses

The organisational impact of good editing tooling goes beyond individual productivity.

Improving Content Operations

When content teams can publish independently, without routing every update through a developer, publishing velocity increases significantly. Teams comparing WYSIWYG HTML editors for content teams can also review how visual editors differ from code editors in terms of workflow, usability, and control. Small updates that used to take days (waiting for developer availability, code review, deployment) can happen in minutes. Over time, this compounds into a meaningful operational advantage.

Supporting Team Collaboration

Modern content is rarely created by one person. WYSIWYG editors support collaborative workflows by making content accessible to reviewers, editors, and approvers who may not be technical. A manager can review a draft and make changes directly. A legal team can edit a compliance notice without filing a ticket. The editing environment becomes a shared space rather than a gated one.

Enhancing Digital Experiences

Well-formatted content performs better. It is more readable, more accessible, and more likely to engage visitors. When the tools make it easy to produce high-quality output consistently, the end result is a better digital experience for readers, customers, and anyone interacting with the content.

Key Features to Look for in a WYSIWYG Editor

Not all editors are equal. Here is what separates good choices from great ones.

Ease of Use

The entire value proposition of a WYSIWYG editor is that it is easy to use. If your team needs significant training to operate it, or the interface is cluttered and confusing, it has failed at its core job. Look for clean toolbar design, sensible defaults, and an interface that feels immediately intuitive.

Customisation Capabilities

Your needs today are not your needs in two years. A good editor should let you configure the toolbar to match your use case, surface the features your team actually uses, hide the ones they do not, and extend the editor with custom plugins or integrations when needed. Brand-aligned styling controls and configurable behaviour are signs of a mature, flexible tool.

Security and Reliability

This one matters more than it often gets credit for. Content editors are a vector for XSS attacks and malicious input if they are not built carefully. Look for editors with robust HTML sanitisation, configurable content policies, and a clear security track record. Consistent, stable performance under load is equally important for any production application.

Common Misconceptions About WYSIWYG Editors

A few things that often get said, and why they are not quite right.

“They Are Only for Beginners”

WYSIWYG editors are designed to be accessible to beginners, but they are used heavily by enterprises, product teams, and professional content operations. Tools like Froala are embedded in production applications serving millions of users. Ease of use does not mean limited power.

“They Replace Developers”

They do not. WYSIWYG editors handle the content creation layer; developers still configure, integrate, and extend them. The relationship is complementary. A well-chosen editor actually reduces low-value developer work (fixing someone’s broken HTML) while freeing them for higher-impact tasks.

“All WYSIWYG Editors Are the Same”

The range of quality and capability across editors is wide. Differences in performance, extensibility, security practices, mobile support, and integration depth are significant, especially at scale. Choosing the right editor for your stack and your team size is a real decision with real trade-offs.

Choosing the Right WYSIWYG Editor

Here are some tips for choosing the right WYSIWYG editor:

Understanding Your Needs

Start with your team and your context. A small blog has different requirements from a B2B SaaS platform with tens of thousands of users generating content. Consider team size, content volume, technical environment, and who will actually be using the editor day-to-day. For developers working across different operating systems, exploring Linux-compatible HTML editors can help you understand which features, integrations, and cross-platform capabilities best support your development workflow.

Evaluating Feature Sets

Core formatting and media management are table stakes. Beyond that, look at whether the editor supports the advanced features your content requires: tables, custom embeds, specialised formatting, and whether it offers a clean source code view for users who need it. API access and headless compatibility matter if you are building a custom application.

Planning for Long-Term Success

An editor you have to rip out in 18 months is a liability. Look at the vendor’s update cadence, community health, documentation quality, and enterprise support options. If your team is considering switching editors, a Migrate from Lexical to Froala process can help evaluate compatibility, implementation requirements, and the steps needed for a smoother transition. An actively maintained editor with a strong developer ecosystem is a safer long-term bet than a lighter tool that may not keep pace with your needs.

Conclusion

A WYSIWYG editor is one of those tools that quietly underpins a huge amount of the content on the internet. Every time someone formats a blog post, updates a landing page, or writes a knowledge base article without touching HTML, there is likely a WYSIWYG editor doing the work behind the scenes.

Understanding what these editors are, how they work, and what to look for when choosing one is genuinely useful, whether you are a writer looking for a better environment to work in, a marketer trying to move faster, or a developer deciding which component to embed in your application.

Froala was built with all of these use cases in mind: a lightweight, powerful, and developer-friendly editor that does not sacrifice ease of use for capability. Explore Froala to see how a well-engineered WYSIWYG editor can support content teams, developers, and product workflows in practice.

Frequently Asked Questions

What is a WYSIWYG editor?

A WYSIWYG editor is a visual content editing tool that shows you a live, formatted preview of your content as you create it. The acronym stands for “What You See Is What You Get,” meaning the editing view matches what your audience will see when the content is published, no HTML knowledge required.

Do I need coding skills to use a WYSIWYG editor?

No. WYSIWYG editors are specifically designed so that coding knowledge is optional. You interact with a visual toolbar, and the editor handles HTML generation for you. That said, most good editors also include a source code view for users who do want to write or inspect HTML directly.

What is the difference between a WYSIWYG editor and an HTML editor?

A WYSIWYG editor provides a visual editing interface where formatting is applied by clicking toolbar controls. An HTML editor requires you to write raw HTML markup and only shows the rendered result after saving or previewing. WYSIWYG editors are suited to content teams; HTML editors are suited to developers who need granular control.

Where are WYSIWYG editors commonly used?

WYSIWYG editors are embedded in content management systems (like WordPress and Contentful), business applications (knowledge bases, documentation platforms, support tools), website builders, and SaaS products that include user-generated content features.

Why do businesses use WYSIWYG editors?

Businesses use WYSIWYG editors to speed up content publishing, reduce dependency on development teams for routine updates, maintain consistent formatting across content, and enable more team members to contribute content without technical training.

Creating a Side-by-Side AI Assist Chat with Gemini Integration

Side-by-Side AI Assist Chat

The Froala’s AI Assist feature lets users generate or rewrite content through natural language. However, its default popup behavior can obscure the editing area. This creates friction when users need to see their original text while chatting with AI.

This article explains a complete working prototype that solves this problem. It repositions the AI Assist interface into a dedicated side panel. The editor and chat sit side-by-side in a clean flex layout. The implementation uses Froala’s built-in customization hooks, DOM manipulation, and direct integration with Google’s Gemini model for rapid prototyping.

Key Takeaways

  • Froala’s event system and DOM flexibility allow you to reposition the AI Assist popup from a floating modal into a fixed sidebar without touching the editor’s core logic.
  • A simple flex layout combined with CSS overrides and the popups.show.aiAssist.promptPopup event creates a seamless side-by-side editing experience.
  • The aiAssistRequest callback makes it straightforward to integrate any LLM—this prototype uses Gemini, but the same pattern works with Claude, OpenAI, or your own backend.
  • Froala remains fully functional when you customize the AI interface; other features like image uploads, formatting, and toolbars continue to work normally.
  • Client-side prototyping is fast, but production deployments should proxy API calls through a backend and add authentication, rate limiting, and error handling.

AI Assist Chat with Gemini Integration

Why Froala’s Flexibility Matters

Froala is not just a WYSIWYG box. It ships with a modular architecture that lets developers control nearly every visual and behavioral aspect. You can:

  • Add or remove toolbar buttons
  • Register custom commands
  • Listen to lifecycle events
  • Override default popup positioning and styling

The AI Assist feature follows the same philosophy. Instead of locking developers into a single interaction pattern, Froala exposes aiAssistRequest as an async callback and fires specific popup events (popups.show.aiAssist.promptPopup and popups.hide.aiAssist.promptPopup). These hooks make the interface adaptable to different UX requirements.

Other powerful features visible in this prototype include third-party image handling (Filestack + Filerobot), code highlighting via Prism, and PDF export via html2pdf library. These demonstrate how Froala acts as a central hub rather than an isolated component.

The Problem with Default AI Popups

Most AI chat interfaces in editors appear as floating modals or side popovers. While convenient for quick prompts, they cover the document when open. Users often want to:

  • Compare AI suggestions with existing paragraphs
  • Keep context visible while iterating
  • Maintain a persistent chat history next to their work

The code in this article addresses exactly that need. It transforms the AI prompt interface from an overlay into a fixed-width sidebar that remains visible alongside the editor.

High-Level Solution Overview

The approach combines four techniques:

  1. A flex-based container that reserves space for both the editor and the chat panel.
  2. Custom CSS that overrides the default .fr-ai-assist-prompt-popup styles when it is moved.
  3. Event listeners that detect when the AI popup opens or closes.
  4. Simple DOM relocation that moves the popup element into the sidebar container.

Because the relocation happens after Froala renders the popup, the editor’s internal state remains untouched. The AI logic itself is handled through the aiAssistRequest option, keeping the integration clean.

Step-by-Step Implementation

To help you visualize the end result before diving into the technical details, the full working code for this prototype is available on JSFiddle. You can open the fiddle, interact directly with the side-by-side AI chat, test different prompts, and see the layout behavior in real time. This hands-on preview makes the following explanations much easier to follow. 

Step 1: Create the Layout Container

The HTML starts with a simple flex container:

<div class="container">

   <div id="editor-example" class="col"></div>

   <div id="chat-popup" class="col chat-ui"></div>

</div>

The .container uses display: flex and centers itself with margin: auto 20%. The editor receives the main editing area. The #chat-popup div starts empty and acts purely as a destination for the relocated AI interface. Setting explicit widths (260px for the chat) and flex-grow: 0 keeps the sidebar stable while the editor can grow.

This structure is intentional. It avoids fighting Froala’s internal layout system. Instead, we let the editor render normally inside #editor-example and only move the AI popup element after it appears.

Step 2: Load Required Stylesheets and Scripts

The prototype loads several libraries to showcase Froala’s ecosystem:

  • Froala’s main CSS and JS packages
  • Prism for syntax highlighting (useful when AI returns code blocks)
  • Filestack and Filerobot for advanced image handling
  • DOMPurify for safe HTML insertion

These dependencies are not strictly required for the side-by-side AI feature. They are required to load full featured Froala instances.

The critical CSS for the side-by-side experience lives in the <style> block:

.container { display: flex; margin: auto 20%; }

#chat-popup { width: 260px; flex-grow: 0; flex-shrink: 0; }

#chat-popup .fr-ai-assist-prompt-popup {

   position: relative !important;

   top: auto !important;

   left: auto !important;

   width: 100%;

   height: 100%;

   box-shadow: unset;

   border: 1px solid #CCCCCC;

   border-radius: 0 10px 10px 0;

   border-left: 0;

}

The !important declarations override Froala’s absolute positioning and box-shadow. When the popup moves into #chat-popup, it fills the entire sidebar height and blends visually with the editor.

Additional rules adjust toolbar border radii when the chat is open:

#editor-example.chat-opened .fr-toolbar.fr-top {

   border-top-right-radius: 0;

}

This small visual tweak removes the gap between the editor and the sidebar, creating a seamless interface.

Step 3: Initialize the Editor with AI Assist

The core logic lives inside DOMContentLoaded:

let editor = new FroalaEditor('#editor-example', {

   placeholder: "Write something .....",

   heightMin: 450,

   key: "******************",

   aiSupplementalTermsAccepted: true,

   aiAssistRequest: async function (data, signal) { ... },

   events: { ... }

});

Several options deserve attention:

  • heightMin: 450 ensures the editor has enough vertical space even when the sidebar is present.
  • aiSupplementalTermsAccepted: true is required to enable AI features in production.
  • The aiAssistRequest callback is where custom backend logic lives.

Step 4: Implement the Gemini Integration

For this prototype, the AI call happens directly from the browser using Gemini’s public API. While not suitable for production (API keys should never be exposed client-side), it demonstrates the integration pattern clearly:

aiAssistRequest: async function (data, signal) {

   const response = await fetch(

       'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + geminiKey,

       {

           method: 'POST',

           headers: { 'Content-Type': 'application/json' },

           signal,

           body: JSON.stringify({

               contents: [{

                   parts: [{

                       text: `

                           Return only HTML.

                           Instruction: ${data.prompt}

                           Content: ${data.text || ''}

                       `

                   }]

               }]

           })

       }

   );

   const result = await response.json();

   return result.candidates[0].content.parts[0].text;

}

Key design choices:

  • The prompt explicitly instructs “Return only HTML.” This prevents the model from wrapping responses in markdown or explanations.
  • The signal parameter is forwarded to fetch, allowing Froala to cancel requests when the user closes the prompt.
  • data.prompt contains the user’s instruction. data.text holds any currently selected content, giving the model context.

Froala expects the callback to return a string of HTML. The Gemini response is passed through directly, which the editor then inserts into the document.

Step 5: Reposition the AI Popup Using Events

The real customization happens in the events object:

events: {

   'popups.show.aiAssist.promptPopup': function () {

       document.getElementById('editor-example').classList.add('chat-opened');

       repositionAiPopup();

   },

   'popups.hide.aiAssist.promptPopup': function () {

       document.getElementById('editor-example').classList.remove('chat-opened');

   }

}

When the popup appears, we add the chat-opened class (for border styling) and immediately call repositionAiPopup().

The reposition function is simple but effective:

function repositionAiPopup() {

   const elementToMove = document.querySelector('.fr-ai-assist-prompt-popup');

   const chatContainer = document.querySelector('#chat-popup');

   if (elementToMove && chatContainer) {

       chatContainer.appendChild(elementToMove);

   }

}

Froala creates the .fr-ai-assist-prompt-popup element dynamically and appends it to the body or editor container. By moving it with appendChild, we change its visual parent without breaking any internal references the editor holds. The CSS rules we defined earlier then take over and make the element fill the sidebar.

When the user closes the prompt, we simply remove the class. The next time the popup opens, the function runs again and moves the element back into the sidebar.

Why This Approach Works So Well

This technique leverages Froala’s event system and the fact that popups are regular DOM elements. No plugin registration or deep internal hacking is required. The same pattern can be adapted for:

  • Moving the popup to a completely separate modal
  • Docking it to the bottom of the screen
  • Integrating it with a custom chat UI built in React or Vue

The editor remains fully functional. All other toolbar commands, image uploads, and formatting options continue to work normally.

Additional Editor Capabilities Demonstrated

Although the focus is AI customization, the prototype includes several other Froala strengths:

  • Image editing: The Filerobot integration (via imageFileRobot.min.js) lets users perform advanced edits without leaving the editor.
  • File handling: Filestack drag-and-drop support enables direct uploads from cloud storage.
  • Content safety: DOMPurify is loaded to sanitize any AI-generated HTML before insertion.
  • Code support: Prism and Codemirror scripts prepare the editor for rich code block handling when AI returns technical content.

These features show that customization of the AI interface does not come at the cost of other capabilities.

Testing and Prototype Considerations

Because this is a frontend prototype, the Gemini key is stored in a JavaScript variable. In a real application you would:

  1. Create a lightweight backend endpoint that proxies requests to Gemini (or any other model). Learn more on this “setup AI React WYSIWYG editor powered by Claude” guide.
  2. Add authentication and rate limiting.
  3. Store conversation history server-side if needed.
  4. Use the signal parameter to properly handle request cancellation.

The current setup is excellent for rapid iteration and internal demos. You can test different prompt templates simply by changing the string inside aiAssistRequest.

Best Practices for Production

When moving this pattern to production:

  • Keep API keys server-side.
  • Add error handling inside aiAssistRequest and return user-friendly messages.
  • Consider debouncing or throttling rapid AI calls.
  • Test the reposition logic across different screen sizes. The 260px sidebar may need to become responsive.
  • Listen for window resize events if you want the sidebar to collapse on mobile.

Froala’s event system makes all of these extensions straightforward.

Conclusion

Froala Editor’s strength lies in its willingness to get out of the developer’s way. The AI Assist feature is powerful out of the box, yet it remains fully customizable through a small set of options and events.

By combining a flex layout, targeted CSS overrides, and the popups.show.aiAssist.promptPopup event, we transformed the default floating chat into a persistent side-by-side companion. The Gemini integration required only a few lines inside aiAssistRequest, proving that connecting modern LLMs is fast when the editor exposes the right hooks.

This approach demonstrates the broader philosophy behind Froala: give developers control over both content editing and the surrounding user experience. Whether you need a side panel, a bottom dock, or a completely custom AI workflow, the same principles apply.

The full code is self-contained and easy to run locally. Download Froala, clone the code, replace the placeholder Gemini key, and experiment with different prompt structures. You will quickly see how little code is needed to shape the editor into exactly the interface your users need.

Try the pattern in your own project and share what custom layouts you create. The editor’s flexibility makes almost any UI variation possible.

graphical user interface, text

HTML Editor Software Compared for Desktop, Cloud, and Embedded Use

Three glowing icons connected by luminous lines forming a triangle, with an embedded chip at the top, a cloud upload icon at the bottom left, and a desktop monitor at the bottom right, representing the three main types of HTML editor software and their relationship to one another.

HTML editor software covers more ground than most teams realize. For example, a developer building a web application needs different tools compared to a marketer updating a landing page. Both work differently from a SaaS team embedding an editor directly into their product. Matching each of these roles to the right tool is what our guide to how to Pick the Best WYSIWYG HTML Editor for Your Use Case works through, use case by use case.

To reflect the need to address different requirements, different types of editors emerged. Desktop editors handle complex local development, while cloud editors make browser-based editing genuinely capable. Embedded editors power content creation inside the products users already work in.

Understanding how each type differs helps you choose one that fits your team’s actual workflow. This article will help you compare your options when looking for an HTML editor for your requirements.

Key Takeaways

  • Desktop editors offer the deepest control and performance for developers, but they’re tied to a single machine and require manual upkeep.
  • Cloud editors prioritize accessibility and collaboration, letting teams work from any device without installation or file syncing.
  • Embedded editors integrate directly into web applications, giving end users formatting capability without leaving the platform.
  • The right editor type depends on who is doing the work, not on which tool has the longest feature list.
  • Many organizations benefit from using more than one type, deploying each where it fits the workflow rather than applying one tool across all tasks.

What Is HTML Editor Software?

HTML editor software refers to any tool that lets users create, edit, and manage HTML content. It covers everything from lightweight browser tools to full development environments and embedded application components.

Understanding HTML Editor Software

At its core, HTML editor software sits between a user and the markup that browsers render. Code-based editors let users write and modify HTML directly, with syntax tools to support accuracy. Visual editors, or WYSIWYG editors, let users format content through an interface that mirrors the final output, generating the corresponding HTML automatically.

The distinction matters because it determines who can use the tool productively. For instance, a developer is comfortable writing markup directly. Similarly, a content manager or marketer typically isn’t, and a visual editor removes that barrier without sacrificing output quality.

Common Features of HTML Editor Software

Most HTML editors, regardless of type, share a core set of capabilities. Syntax highlighting color-codes markup elements to improve readability and reduce errors. Code assistance suggests valid tags and attributes as users type.

A glowing multi-layered digital structure under construction, with editor interface panels representing visual editing, code editing, and media management visible across its levels, illustrating the shared core features that form the foundation of all HTML editor software types.

Rich text and visual editing tools cover the needs of non-technical users. Live preview shows how content renders before publishing, and media management handles image and file insertion.

Why Businesses Use HTML Editor Software

Teams use these tools to remove bottlenecks from content workflows. For example, non-technical contributors can format and publish without developer support. Likewise, developers get precise environments suited to their work. No matter the user, the right editor shortens the path from content decision to live output.

Types of HTML Editor Software

The three main categories serve distinct workflows and user types. Choosing between them starts with understanding what each one primarily focuses on.

Desktop HTML Editors

Desktop editors install directly on a local device and run without a browser or internet connection. They’re built primarily for developers who need a full-featured coding environment.

VS Code, Sublime Text, and similar tools fall into this category. They offer deep customization through extensions, local file management, and direct integration with version control systems.

Cloud-Based HTML Editors

Cloud editors, or online HTML editors, run entirely in a browser. Users access their projects from any device with an internet connection, and the editor stores files remotely. This model eliminates installation and makes collaboration straightforward, since multiple users can access the same project without syncing files. CodePen and similar platforms are well-known examples of browser-based HTML editing environments.

Embedded HTML Editors

Embedded editors are components integrated directly into a web application or platform. Rather than existing as standalone tools, they live inside a product and serve that product’s users.

A CMS editor, an email builder’s formatting interface, or a documentation platform’s content creation field all rely on embedded HTML editors. With these types of editors, users create and edit content without ever leaving the application.

Desktop HTML Editor Software Explained

Desktop editors remain the standard for development teams working on large, complex projects where local control matters.

Key Features of Desktop HTML Editors

Debugging tools identify errors before they reach a browser. Linting flags markup issues as developers write. Extensive plugin ecosystems let teams configure their environment for their specific stack.

Advantages of Desktop HTML Editors

Performance is the clearest advantage. Desktop editors handle large codebases without the constraints browser-based environments face. Thus, full offline functionality means work continues regardless of network availability.

Limitations of Desktop HTML Editors

Desktop editors require installation and are bound to the machine they’re on. As a result, collaboration requires additional tooling, such as version control and file sharing, layered on top.

Cloud-Based HTML Editor Software Explained

Cloud editors have become capable enough to handle a wide range of editing tasks, and their accessibility advantages make them increasingly common.

How Cloud HTML Editors Work

Cloud editors run inside a browser and store files remotely. In this setup, users open a URL and start working immediately. Updates are also visible to all collaborators instantly.

Benefits of Cloud-Based HTML Editors

Remote accessibility is the primary benefit. Teams contribute to shared projects from any device without syncing files. Maintenance also drops, since editor updates happen server-side automatically.

Challenges of Cloud-Based HTML Editors

Cloud editors depend on a stable internet connection. Files live on external servers, which raises data security considerations. Some platforms also limit customization compared to desktop alternatives.

Embedded HTML Editor Software Explained

A glowing CMS dashboard interface with an embedded HTML editor panel open at its center, showing a formatting toolbar and structured content alongside surrounding dashboard panels for analytics, media, and user activity, representing how embedded editors integrate into web applications.

Embedded editors exist to serve users within a specific application, not as standalone tools.

What Makes Embedded Editors Different

You can treat embedded editors as more of a component than a product. It ships inside an application and serves that application’s users. Furthermore, the interface prioritizes visual ease over technical depth, and the application controls what features are available.

Common Use Cases for Embedded HTML Editors

CMS platforms embed editors so content teams can publish without leaving the dashboard. Email builders use them for visual campaign design. SaaS platforms embed them to give users formatting capability within the product itself.

Benefits of Embedded HTML Editors

Embedded editors keep users inside the application, reducing context-switching. Non-technical users gain formatting capability without understanding HTML. For product teams, integrating an HTML editor is often faster than building content editing from scratch.

Challenges of Embedded HTML Editors

Security is critical when embedding HTML editors. As such, embedded editors must sanitize all user-generated HTML before it reaches storage to prevent XSS attacks. Performance across devices also requires ongoing testing, especially if you’re building your own editor.

Note: XSS (Cross-Site Scripting) occurs when malicious scripts in user-generated content execute in another user’s browser. Sanitizing HTML output before storage neutralizes this risk.

Comparing Desktop, Cloud, and Embedded HTML Editor Software

Each editor type makes different tradeoffs across the dimensions that matter most to teams evaluating their options.

Ease of Access and Deployment

Desktop editors require installation and work only on the local machine. In contrast, cloud editors deploy instantly through a browser, and you can access them from anywhere. Lastly, embedded editors work directly inside an application, requiring no separate tool access at all.

Collaboration and Team Workflows

Cloud and embedded editors support multi-user access naturally. In these setups, teams can contribute to shared content without syncing files or managing version conflicts manually. 

On the other hand, desktop editors are primarily individual tools. Collaboration requires external version control and file-sharing systems, which could introduce process overhead.

Performance and Scalability

Desktop editors handle large, complex local projects with strong performance. Cloud editors scale for distributed teams and growing content libraries without adding local storage requirements.

Embedded editors scale with the application they live in. As the product grows, the editor grows with it. This means that it can serve more users and content without requiring a separate infrastructure investment.

Customization and Integration

Desktop editors offer the deepest developer customization through plugins, settings, and environment configuration. Embedded editors support API and workflow integrations that connect the editor’s output to databases, publishing pipelines, and other application systems. Cloud editors sit in the middle, offering convenience, usability, and some flexibility but typically less extensibility than the other two.

How to Choose the Right HTML Editor Software

Ultimately, the right choice depends on your team’s actual needs, not on feature lists.

Evaluate Your Team’s Technical Needs

First, identify who will use the editor and what they need to make (e.g., content, webpages, etc.). Developers building applications fit desktop editors naturally and might need cloud editors for testing. Content teams without coding knowledge need visual interfaces. Mixed teams often need both.

Consider Collaboration Requirements

Multiple glowing cursor arrows converging from different directions onto a single shared glowing document, representing multiple contributors accessing and editing the same content simultaneously in a collaborative HTML editing environment.

Remote teams benefit most from cloud and embedded editors. Assess how each type handles concurrent access and review workflows before deciding. Otherwise, file-syncing overhead, which is a real friction point for distributed teams, might happen.

Analyze Security and Compliance Needs

Desktop editors keep data local, which simplifies compliance in some industries. Cloud editors store data externally, requiring evaluation of the provider’s security practices. Embedded editors require server-side sanitization of all user-generated HTML.

Plan for Long-Term Scalability

Consider where your content operation will be in two or three years. Afterwards, choose an editor type that accommodates growth rather than one that requires replacement as your needs evolve.

Why Modern Businesses Prefer Flexible HTML Editor Software

Teams that operate efficiently tend to use editor software that adapts to different workflows.

Supporting Hybrid Workflows

Most content operations involve both technical and non-technical contributors. Flexible HTML editor software accommodates both without forcing either group into a tool that doesn’t fit their role. In turn, it improves collaboration between departments and streamlines publishing processes.

Accelerating Digital Content Production

Bottlenecks typically come from handoffs between contributors. Editor software that gives each person the right interface reduces those handoffs and shortens the path to published output. As a result, you can enable agile content operations more easily.

Enhancing User Experience

An editor that matches its users’ skill level helps reduce errors and frustration. Visual interfaces simplify formatting for non-technical contributors, while responsive editing support helps ensure content looks correct across screen sizes.

Modern HTML editor software should handle both, making accessible and device-ready content creation achievable for every type of user.

Conclusion

Desktop, cloud, and embedded HTML editors each solve a real problem for a specific kind of user. Desktop editors serve developers who need control and performance. Cloud editors serve teams that need accessibility and collaboration. Embedded editors serve applications that need to put content creation in front of end users without friction.

Most organizations eventually need more than one. So, choosing the right combination starts with being honest about who does the work, where they do it, and what they need to produce.

Ready to find the right HTML editor software for your team? Froala offers a lightweight, embeddable editor with clean HTML output, framework compatibility, and a visual interface that works for both developers and content teams. Try it and see how it fits your workflow.

Frequently Asked Questions

What is HTML editor software?

HTML editor software lets users create, edit, and manage HTML content. It ranges from developer-focused desktop tools to visual embedded editors for different types of users.

What is the difference between desktop and cloud HTML editors?

Desktop editors install locally and run offline, offering strong performance and deep customization. Cloud editors run in a browser and support remote collaboration without installation.

What are embedded HTML editors used for?

They power content creation inside web applications: CMS platforms, email builders, documentation tools, and SaaS products with user-generated content.

Which type of HTML editor software is best for collaboration?

Cloud and embedded editors support collaboration most naturally. Both allow shared access without file syncing, making them better suited for distributed teams.

How do businesses choose the right HTML editor software?

Start by identifying both your users and your requirements. Then, match the editor type to the workflow, not just the feature list. Note that many organizations deploy more than one type.

graphical user interface, text

I Built a Vim Mode for Froala to Show What the Plugin API Can Do

froala vim

Most teams choose a rich text editor by scanning the default feature list. The real test comes later, once your product needs something the editor never shipped with. That is when the plugin API starts to matter.

So I ran an experiment. I built modal, Vim-style editing for Froala as a single-file plugin, with one script tag and no build step. To be clear up front, this is not an official Froala feature and it is not on the roadmap. It is a personal project I put together to see how far the plugin model goes, and to give you a concrete sense of what you could build yourself.

If you have spent time in Vim, the appeal is familiar. Your keyboard does different things depending on the mode you are in. You move, delete, and change text while your hands stay on the home row. This plugin brings that same flow into a Froala editor, running as plain Froala plus one extra script.

Disclaimer: this is not an official Froala Plugin, this was made by the internal team to speed up our workflow and to show how Froala APIs work.

Key Takeaways

  • The plugin API matters more than the feature list — editors look alike until your product needs custom behavior the editor never shipped with.
  • A full Vim mode fits in one file, no build step — single script tag, self-registers, adds its own toolbar command.
  • It covers everyday Vim, on purpose — modes, motions with counts, visual operators, doubled commands, line jumps; deeper grammar left out to stay auditable.
  • Built on solid primitives — motions ride on Selection.modify(), and every edit respects Froala’s undo stack.
  • A small runtime API drives it from your code — enable(), disable(), toggle(), getMode(), and disabling restores vanilla Froala.

What modal editing brings to a WYSIWYG

Vim splits editing into modes, and this plugin keeps the three that matter day to day.

 

In normal mode, your keys run commands. You move with h j k l, jump by word with w b e, and delete a line with dd. In insert mode, the keyboard types text the way a WYSIWYG normally does. In visual mode, you select a range and then act on it with one key, such as d to delete or y to yank.

 

A small badge sits in the bottom-left corner of the editor and shows the current mode, so you always know which keys are live. The block cursor and the badge share a configurable color, which makes the active mode easy to read at a glance.

Why extensibility matters when you compare editors

When you put Froala next to other editors, the headline feature lists look similar. The difference shows up the day your product needs custom behavior, and the cost of that difference lands on your team.

 

A modal editing layer is a good stress test for a plugin API. It intercepts keystrokes, tracks state, moves the caret, mutates content, and talks to the undo manager. The fact that I could build all of that in a single, readable file, on my own time, tells you the architecture is open and approachable. That same door stays open for the features you actually need, whether that means custom toolbar buttons, custom commands, or custom keybindings tied to your own product shortcuts.

 

For a bottom-of-funnel decision, this is the signal worth weighing. Froala gives you an editor that works out of the box and a plugin model you can grow into, as this side project shows.

How the plugin works under the hood

The plugin self-registers on FroalaEditor.PLUGINS.vim and adds a vim toolbar command when it loads. You add it to a page by dropping one script after the Froala bundle.

 

<link href="froala_editor.pkgd.min.css" rel="stylesheet">

<script src="froala_editor.pkgd.min.js"></script>

<script src="froala.vim.js"></script>

<script>
  new FroalaEditor('#editor', {
    toolbarButtons: ['bold', 'italic', '|', 'vim'],
    vim: {
      enabled: true,
      navLayout: 'hjkl',   // 'hjkl' or 'ijkl'
      leader: ',',
      startMode: 'normal',
      cursorColor: '#0098F7',
      showLineNumbers: false
    }
  });
</script>

 

 

Three design choices keep it reliable and small.

Motions ride on the browser’s selection API

Caret movement runs through Selection.modify(alter, direction, granularity), the same primitive the browser uses internally. As a result, motions follow soft-wrapped lines correctly, and j lands where a human expects it to. The API is non-standard, yet it is implemented in Chromium, WebKit, and Firefox, which covers every browser Froala supports. When modify throws on an edge case, that one motion becomes a no-op and the rest of the key map keeps working.

Edits respect Froala’s undo stack

Every mutation pushes a pre-state and a post-state onto Froala’s undo manager. Because of that, a single u reverses exactly one operator, and Ctrl-r redoes it. Your editing history stays clean whether the change came from the toolbar or from a Vim command.

One runtime API for everything

The plugin exposes a small API so you can drive it from your own code.

const ed = new FroalaEditor(‘#editor’, { /* … */ });

ed.vim.enable(); // turn vim keybindings on
ed.vim.disable(); // back to vanilla Froala
ed.vim.toggle();
ed.vim.getMode(); // ‘normal’ | ‘insert’ | ‘visual’
ed.vim.setLeader(‘\\’); // change the leader at runtime
ed.vim.setCursorColor(‘#15B69B’);
ed.vim.toggleLineNumbers(); // also bound to leader ‘n’

When you disable it at runtime, the editor returns to standard Froala behavior, with the keystroke interception, the badge, and the block cursor all removed.

See it in action

Drop in your screen recording of the plugin running, with the on-screen keyboard showing each keystroke. An interactive key-cast demo ships alongside this article if you want a self-running visual in the meantime.

The key map at a glance

table vim

What the plugin focuses on

The plugin covers the part of Vim you reach for every day: modes, the common motions with count prefixes, the visual-mode operators, the doubled commands, line jumping, an optional line-number gutter, and a configurable leader.

 

The deeper Vim grammar stays out of scope on purpose. Operator-plus-motion composition like dw, named registers, marks, macros, the . repeat, and / search all sit outside the spec. Keeping that boundary is what lets the whole thing stay one readable file you can audit and extend yourself.

Try the same approach yourself

 

If you want to wire this kind of behavior into your own editor, the pattern is the same one I followed. For another hands-on example, learn how to build a Paint Format plugin using Froala’s plugin APIs, command registration, event handling, and state management. The Froala Claude Code plugin can provide guidance for registering custom plugins, creating toolbar commands, binding shortcuts, and working with the editor API. First, load the Froala bundle on your page. Next, add your plugin script after it so it can register on FroalaEditor.PLUGINS. Finally, expose a toolbar command and a small runtime API, then set your preferences in the Froala init. From there, the toolbar button toggles your feature and the API lets you wire it to anything else in your app.

 

If you are evaluating Froala right now, building a small plugin like this is a good way to see how far the model goes before you commit. Start a free Froala trial and try the editor with your own content, browse the Github Repo to see the full API, or review Froala pricing when you are ready to ship.

 

Explore a Vim-style Froala plugin that shows how the editor’s plugin API supports custom keybindings, toolbar commands, undo handling, and extensibility.

graphical user interface, text

HTML WYSIWYG vs Code Editors: Which Is Better for Modern Content Teams?

A glowing balance scale with a paintbrush on the left pan and a scalpel on the right, representing the tradeoff between the visual ease of an HTML WYSIWYG editor and the surgical precision of a code editor.

The tools a content team uses determine how fast ideas become published pages. Two categories dominate that conversation: HTML WYSIWYG editors and code editors. The former lets users format content visually, while the latter gives developers direct access to markup. 

Both produce web content and have legitimate use cases, but the question is which one fits your team’s actual workflow. For example, marketing teams pushing daily campaign content have different requirements than a development team building a custom web application. 

Understanding where each tool excels and where it falls short makes the decision clearer than any feature comparison chart.

Key Takeaways

  • HTML WYSIWYG editors generate structured HTML automatically from visual input, making content creation accessible to non-technical contributors without any coding knowledge.
  • Code editors give developers direct, unabstracted access to markup and tooling, making them the appropriate choice for complex applications, framework-based development, and performance-sensitive builds.
  • The core difference between the two isn’t capability; it’s the skill level of the intended user and the nature of the work being done.
  • Most modern organizations benefit from using both tools at different layers, with WYSIWYG editors handling content operations and code editors handling application development and customization.
  • Editors that support both a visual surface and a code view offer the most flexibility for mixed-skill teams, allowing each contributor to work in the mode that matches their expertise.

What Is an HTML WYSIWYG Editor?

HTML WYSIWYG editors present content creation as a visual activity. You format what you see, and the editor generates the corresponding HTML automatically. 

Understanding Visual HTML Editing

WYSIWYG stands for “What You See Is What You Get.” The editing surface mirrors the published output directly, so users format content visually without writing or reading HTML. For instance, clicking the underline button wraps text in a <u> tag, and selecting a heading applies the correct element. The HTML generates in the background, invisibly, as users work.

This approach makes structured content creation accessible to anyone comfortable with a word processor. As a result, the gap between editing and publishing significantly shrinks.

Common Features of HTML WYSIWYG Editors

A glowing artist's palette displaying formatting icons including bold, italic, headings, alignment, lists, links, and media controls, representing the rich set of visual editing tools available in an editor.

Most capable WYSIWYG editors include rich text formatting controls covering headings, lists, alignment, tables, and inline styles. They also support drag-and-drop functionality that lets users insert images and videos without navigating file systems manually. 

Furthermore, a live preview confirms how content will render before it publishes. Toolbar-based controls keep all formatting options visible and accessible without requiring keyboard shortcuts or command-line knowledge.

Typical Users of HTML WYSIWYG Editors

WYSIWYG editors see a lot of use from different types of users. Content marketers use them to draft and publish campaign pages without developer support. Similarly, editors and publishers use them to manage article formatting at volume.

Non-technical business teams also use them to update website content independently. Collaborative content departments use them to create a shared editing environment where contributors of varying skill levels can participate equally. Nearly any individual or team that requires web content creation or editing uses a WYSIWYG editor of some sort.

What Is a Code Editor?

Code editors operate at the source level. They give users direct access to the HTML, CSS, and JavaScript that define a page’s structure, style, and behavior.

Understanding Code-Based Editing

A code editor is a text-based environment where users write and modify markup directly. There’s no visual layer between the developer and the output, so what you type is exactly what the browser receives. Developers use code editors when the structure of a page matters at a level that visual tools can’t reliably control.

The tradeoff is that code editors require fluency in HTML, CSS, and often JavaScript. Users who don’t have that background can’t use them productively.

Common Features of Code Editors

Syntax highlighting color-codes different elements of the markup, making code easier to scan and errors easier to spot. Auto-completion suggests valid tags, attributes, and values as users type. Debugging and linting tools flag errors before they reach a browser. File management handles multi-file projects, and version control integration connects editors to Git workflows for tracking changes over time.

Note: Linting is the process of automatically analyzing code for errors, style violations, or potential problems before it runs. A linter catches issues like unclosed tags or invalid attribute values that would otherwise cause rendering problems in the browser.

Typical Users of Code Editors

Software developers use them for full-stack development across HTML, JavaScript (and other front-end frameworks), and backend languages. Technical content creators who manage templates or custom page layouts rely on them for precision edits. Development teams building complex applications use them as the single source of truth for all code changes.

HTML WYSIWYG vs Code Editors: Key Differences

Two monitors side by side on a dark desk, the left displaying a clean visual content layout representing a WYSIWYG editor interface and the right displaying syntax-highlighted HTML markup representing a code editor, illustrating the core difference in how each tool presents the same content.

These tools differ beyond the technical aspects. They represent entirely different philosophies on who’s driving the work and what they actually need.

Ease of Use

WYSIWYG editors require no coding knowledge. A new team member can start producing formatted content within minutes of opening the interface.

On the other hand, code editors have a steeper onboarding curve. Users need to understand HTML structure, CSS rules, and how browsers interpret markup before they can work efficiently. For non-technical teams, this gap in accessibility is often the deciding factor.

Editing Speed and Productivity

For content creation tasks, WYSIWYG editors are faster. Formatting a blog post, inserting media, and publishing to a CMS takes minutes in a visual editor. The same task in a code editor takes longer and requires more technical attention

For development tasks, the relationship reverses. A developer building a custom layout has more direct control in a code editor than through visual tools that abstract the underlying structure.

Customization and Flexibility

Code editors offer complete control, as every attribute, class, and structural decision is explicit and intentional. WYSIWYG editors trade some of that control for speed and accessibility.

Most professional WYSIWYG editors address this through plugin ecosystems and code view modes. These let developers extend functionality and inspect output directly without abandoning the visual editing surface.

Collaboration and Team Accessibility

WYSIWYG editors create a shared environment that both technical and non-technical contributors can use. A marketer and a developer can both work in the same editor, each contributing within their area of expertise.

Code editors, on the other hand, are naturally developer-centric. This means that non-technical team members can’t meaningfully participate without training. In turn, this limits their effectiveness in mixed-skill organizations.

Benefits of HTML WYSIWYG Editors for Modern Content Teams

Nowadays, content teams are at their most productive when their tool causes minimal friction in their workflow and collaboration.

Faster Content Creation

WYSIWYG editors remove the manual formatting steps that slow publishing cycles. Writers work in a single environment from draft to publish-ready, without exporting, converting, or handing off to a developer for markup. For marketing teams managing high content volumes, that reduction in steps per piece directly improves output capacity.

Lower Technical Barriers

Non-developers can create, edit, and publish structured web content without any HTML knowledge. This independence reduces the engineering team’s involvement in routine content tasks, freeing developers for work that actually requires their expertise. Organizations that route all content changes through a developer unnecessarily create bottlenecks that HTML WYSIWYG editors eliminate.

Improved Collaboration

When reviewers and editors can interact with formatted content directly in the editor, approval cycles move faster. Comments, corrections, and revisions happen in context rather than through annotated documents passed between teams. This shortens the feedback loop between content creation and publication significantly.

Consistent Content Formatting

A glowing industrial printing press receiving varied handwritten documents on the left and producing uniformly formatted clean pages on the right, representing how a shared WYSIWYG editor enforces consistent content formatting regardless of who authored each piece.

Shared editors enforce consistent formatting rules across all contributors. This means that heading hierarchy, paragraph spacing, and link styling follow the same patterns regardless of who is editing. This consistency is difficult to maintain through manual coding, where individual style choices vary between developers.

Benefits of Code Editors for Technical Teams

Good code editors are typically those that help developers focus more, stay organized, and adhere to best practices.

Greater Development Control

Code editors give developers direct access to every page’s structure, so there’s practically no abstraction layer between intent and output. This matters when building layouts that require precise control over HTML attributes, CSS specificity, or JavaScript behavior. Visual tools can’t replicate this level of intentionality for complex builds.

Better Performance Optimization

Developers using code editors control exactly what markup the browser receives. They can minimize unnecessary elements, write optimized CSS, and avoid the bloated output that some WYSIWYG editors generate. This control is significant for performance-sensitive applications where page weight and rendering speed matter.

Advanced Development Features

Debugging tools identify rendering problems before they reach users. And as we discussed earlier, linting catches markup errors during development. Version control integration tracks every change, supports collaboration between developers, and enables rollback when something breaks.

Scalability for Complex Applications

Large-scale web applications with custom architectures, framework dependencies, and complex data structures require code editors. Visual editors operate at the content level, while code editors operate at the application level. The further a project moves from content management toward custom development, the more necessary the precision of a code editor becomes.

When Modern Content Teams Should Choose HTML WYSIWYG Editors

Not every team has the same content demands, but certain environments make the case for WYSIWYG editors clearly.

Content-Heavy Publishing Environments

Blogs, editorial platforms, CMS, LMS, and marketing teams publishing frequently benefit most from visual editors. The volume of content these environments produce makes the speed advantage of WYSIWYG tools directly impactful. With an HTML WYSWIYG editor, every hour saved per piece multiplies across hundreds of published pages per year.

Teams With Non-Technical Contributors

When editors, marketers, and business stakeholders need to create or update content independently, a WYSIWYG editor is the only practical option. Requiring non-technical contributors to learn HTML before updating pages creates unnecessary barriers that slow content operations without improving output quality.

Businesses Prioritizing Workflow Speed

Organizations where time-to-publish directly affects business outcomes benefit from the publishing speed WYSIWYG editors provide. These typically include news outlets, e-commerce companies running promotions, or SaaS businesses managing product documentation. Simplified approvals and direct publishing integrations remove delays between content decisions and live pages.

When Code Editors Are the Better Choice

Some projects, especially those that deeply involve coding and layout customization, sit outside the scope of visual editing tools entirely.

Highly Customized Web Applications

Applications with unique interaction patterns, custom layout systems, or complex front-end architectures require code editors. Visual tools don’t have the precision to build interfaces where every structural decision has downstream consequences for functionality.

Developer-Centric Workflows

Development teams working within framework-based environments, managing component libraries, or integrating with APIs need the full capabilities of a code editor. The tooling, extensions, and integrations that code editors support are purpose-built for this kind of work.

Performance and Security Priorities

Applications where page performance and security require manual control benefit from code editors. Developers can audit every element of the output, remove unnecessary markup, and implement security measures directly in the code. This is in contrast to relying on a tool’s abstraction layer to handle them correctly.

Can Teams Use Both HTML WYSIWYG and Code Editors Together?

Two puzzle pieces, the left containing a visual WYSIWYG editor interface with formatting controls and the right containing HTML and CSS source code, representing how WYSIWYG editors and code editors complement each other in a hybrid content workflow.

Most mature content operations don’t choose one tool exclusively. Instead, they assign each to the tasks it handles best. 

Combining Visual Editing and Developer Control

WYSIWYG editors handle content creation, while code editors handle application development and customization. Marketers use the visual interface to draft and publish. Developers use the code editor to build templates, configure integrations, and extend the editor’s capabilities through its API. Neither group needs to work in the other’s environment.

Hybrid Workflows for Modern Organizations

A common pattern is using a WYSIWYG editor embedded within a CMS for content operations. In this setup, developers maintain the underlying templates and application architecture in a code editor. The two tools operate at different layers of the same product, each serving the team that needs it.

Choosing Tools That Support Both Editing Modes

Editors that include a code view, where users can toggle between the visual surface and the raw HTML source, bridge the gap most effectively. Content editors use the visual interface for formatting. Developers inspect or adjust the source when the visual tools don’t produce exactly the intended output. Modern HTML WYSIWYG editors like Froala support this pattern, offering both editing modes within the same interface.

Conclusion

Choosing between an HTML WYSIWYG editor and a code editor isn’t a binary decision for most organizations. It’s a question of which tool handles which job in your workflow.

Content teams that produce at volume need the speed and accessibility of a visual editor. On the other hand, development teams building and maintaining complex applications need the precision of a code editor. Many organizations need both, deployed at different layers of their content and development operations.

The right starting point is an honest assessment of who on your team creates content, how often they publish, and how much technical knowledge they bring to the task. So, it’s wise to let those answers drive the tooling decision rather than the feature list.

Looking for an HTML WYSIWYG editor that works for both content teams and developers? Froala offers a lightweight, customizable editor with a dual visual and code view, built for teams that need both speed and precision.

Frequently Asked Questions

What is the difference between an HTML WYSIWYG editor and a code editor?

An HTML WYSIWYG editor lets users format content visually through a toolbar interface, generating HTML automatically in the background. A code editor requires users to write HTML, CSS, and JavaScript directly. WYSIWYG editors prioritize accessibility and speed for content creation; code editors prioritize precision and control for development work. 

Are HTML WYSIWYG editors better for content teams?

For most content teams, yes. WYSIWYG editors allow non-technical contributors to create and publish formatted content independently, without developer support.

Why do developers prefer code editors?

Code editors provide direct access to HTML, CSS, and JavaScript without an abstraction layer between intent and output. They support debugging, linting, version control, and framework integrations that visual editors don’t offer.

Can businesses use both WYSIWYG and code editors together?

Yes, and many do. A common approach embeds a WYSIWYG editor within a CMS for content operations while developers maintain templates and application architecture in a code editor. The two tools serve different layers of the same product without competing with each other. 

Which editor type is better for website content management?

For ongoing content management, HTML WYSIWYG editors are generally more practical. They help by allowing content teams to update pages, manage formatting, and publish without developer involvement.

graphical user interface, text