Days
Hours
Minutes
Seconds
x
Skip to content
Froala Documentation

Set up a headless Froala editor

Headless mode turns Froala into a pure editing engine. You design every button, menu, and dialog in your own UI, and Froala handles the editing logic behind them, producing clean and reliable HTML.

Your UI

buttons, menus, dialogs

Froala Action API

commands.bold(), lists.format()

Editable area

Clean HTML

html.get()

What headless mode gives you

Think of a car. The engine does the real work, and the dashboard is how you control it. A regular Froala editor ships with both. Headless mode gives you only the engine, so you can build the dashboard that fits your product.

With headless: true, the editable area and every content-editing method keep working exactly as before. Froala skips drawing its own toolbar, popups, and default box styling, so your design system stays in full control.

Headless mode works well in cases like these.

  • Matching the editor to an existing design system, such as your own React or Vue component library.
  • Placing formatting controls anywhere on the page, like a sidebar, a floating bar, or a command palette.
  • Building a focused writing experience with only the few actions your users need.
Version
Headless mode is available in Froala Editor 5.5.0 and later. Check your version with npm ls froala-editor.

Quick start with vanilla JavaScript

This example builds a working headless editor in one HTML file. Copy it, open it in a browser, and click the buttons.

Load Froala

Use the CDN for a quick test, or install the package for a real project. Pin the same version in both places so your code and your assets always match.

bash

npm install froala-editor@5.5.0

Add the markup and initialize

  1. Add your own toolbar. Each button carries a data-cmd name.
  2. Initialize Froala on a div with headless: true.
  3. Enable the toolbar in the initialized event, once the editor is ready for API calls.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.min.css">
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_style.min.css">
</head>
<body>

  <!-- Your own toolbar. Style it any way you like. -->
  <div id="my-toolbar" role="toolbar" aria-label="Formatting" hidden>
    <button type="button" data-cmd="bold">Bold</button>
    <button type="button" data-cmd="italic">Italic</button>
    <button type="button" data-cmd="ul">Bullets</button>
    <button type="button" data-cmd="center">Center</button>
    <button type="button" data-cmd="undo">Undo</button>
  </div>

  <div id="editor"><p>Start writing here.</p></div>

  <script src="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/js/froala_editor.pkgd.min.js"></script>

  <script>
    const toolbar = document.querySelector('#my-toolbar');

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

      // key: read your license key from config, never commit it

      events: {
        initialized: function () {
          toolbar.hidden = false;
          // the API is ready from here on
        }
      }
    });

    // Map each button name to a Froala Action API call.
    const actions = {
      bold:   () => editor.commands.bold(),
      italic: () => editor.commands.italic(),
      ul:     () => editor.lists.format('UL'),
      center: () => editor.align.apply('center'),
      undo:   () => editor.commands.undo()
    };

    // Keep the text selection in place when a button is pressed.
    toolbar.addEventListener('mousedown', function (e) {
      if (e.target.closest('button')) e.preventDefault();
    });

    // Save an undo step after each change so Undo and Redo can find it.
     function runAction(action) {
     action();
     editor.undo.saveStep();
     }

    toolbar.addEventListener('click', function (e) {
    const btn = e.target.closest('button[data-cmd]');
    if (!btn || !actions[btn.dataset.cmd]) return;
    const cmd = btn.dataset.cmd;
    // Undo and redo move through the history, so they run directly.
   if (cmd === 'undo' || cmd === 'redo') actions[cmd]();
   else runAction(actions[cmd]);
   });
   </script>

</body>
</html>
That is a complete headless editor. The packaged build (.pkgd) includes every plugin, which is why the list and align calls work right away.

Build your own toolbar

Your toolbar is plain HTML that calls Froala methods. Two small habits make it feel as smooth as the built-in one.

Keep the selection on click

When a user presses a normal button, the browser moves focus to that button and the text selection can collapse. Calling preventDefault() on mousedown keeps focus in the editor, so “Bold” applies to the words the user actually selected. Froala’s own toolbar uses the same technique.

For block actions such as align, quote, and lists, Froala 5.5.0 also focuses the editor for you when the selection sits outside it. That gives every action a caret to work from.

Save undo steps around each action

Froala’s built-in toolbar records an undo step for every command. Direct API calls such as commands.bold(), lists.format(), and align.apply() leave that step to you. Call editor.undo.saveStep() after each content change, so Undo reverses exactly one action and Redo can bring it back. Froala saves typing on its own after a short pause, controlled by the typingTimer option.

Wait for the initialized event

Editor methods become available once the initialized event fires. Showing or enabling your buttons inside that event is the simplest way to guarantee every click reaches a ready editor.

Common Action API calls

These methods edit content directly and run normally in headless mode. Use them as the building blocks for your buttons.

BUTTONMETHODDESCRIPTION
Boldcommands.bold()Bold the selected text.
Italiccommands.italic()Italicize the selected text.
Underlinecommands.underline()Underline the selected text.
Strikethroughcommands.strikeThrough()Strikethrough the selected text.
Undocommands.undo()Reverses the most recent action.
Redocommands.redo()Reverses a previous undo action.
Clear formattingcommands.clearFormatting()Removes inline formatting from the selection.
Listslists.format(type)'UL' for bullets, 'OL' for numbers.
Alignmentalign.apply(type)'left', 'center', 'right', or 'justify'.
Quotequote.apply(value)'increase' or 'decrease' the quote level.
Custom styleformat.applyStyle(prop, value)For example, format.applyStyle('color', '#0098F7').
Insert imageimage.insertByURL(url)In headless mode, this accepts the image URL directly, so your own input field or dialog can supply it.

JAVASCRIPT

// Insert an image from your own URL field.
document.querySelector('#insert-image').addEventListener('click', function () {
  const url = document.querySelector('#image-url').value.trim();
  if (url) editor.image.insertByURL(url);
});

// The image loads asynchronously, so record the undo step once it lands.
editor.events.on('image.inserted', function () {
  editor.undo.saveStep();
});
TypeScript

In the 5.5.0 typings, image.insertByURL() is declared with zero parameters, while the runtime accepts a URL in headless mode. Cast the call, for example (editor.image as any).insertByURL(url), until the typings catch up.

Show active button states

Users expect the Bold button to look pressed when the caret sits inside bold text. format.is(tagName) tells you whether a tag applies at the current selection. Refresh your buttons whenever the caret moves or the content changes.

JAVASCRIPT

function refreshButtons(editor) {
  const states = {
    bold:   editor.format.is('strong'),
    italic: editor.format.is('em')
  };

  Object.keys(states).forEach(function (cmd) {
    const btn = document.querySelector('[data-cmd="' + cmd + '"]');
    btn.classList.toggle('is-active', states[cmd]);
    btn.setAttribute('aria-pressed', String(states[cmd]));
  });
}

const editor = new FroalaEditor('#editor', {
  headless: true,
  events: {
    keyup:          function () { refreshButtons(this); },
    mouseup:        function () { refreshButtons(this); },
    contentChanged: function () { refreshButtons(this); }
  }
});
Inside Froala event handlers, this is the editor instance. Setting aria-pressed lets screen reader users hear the button state too.

Style the editing area

In headless mode, Froala adds an fr-headless class to the editor box. That class resets Froala’s default font, color, padding, border, and minimum height, so your page CSS decides how content looks.

Target the inner .fr-element, which is the element users type into. Froala’s reset uses the selector .fr-box.fr-headless .fr-element, so give your rule equal or higher specificity. An ID selector is the simplest choice.

CSS

/* Frame around the editor (a wrapper you own) */
.editor-frame {
  border: 1px solid #E7E9EE;
  border-radius: 10px;
}

/* Typing area. The ID beats Froala's class-based reset. */
#editor .fr-element {
  padding: 16px;
  min-height: 220px;
  font-family: inherit;
  line-height: 1.6;
}

/* Your toolbar's pressed state */
#my-toolbar button.is-active {
  background: #E5F4FD;
  color: #0272B8;
}
Tip
Wrap #editor in a div class="editor-frame" and put the border there. Your frame then stays in your own CSS, fully separate from Froala’s classes.

Read and save content

Content APIs are identical in headless mode. Use html.get() to read the editor’s HTML and html.set() to load saved content.

JAVASCRIPT

document.querySelector('#save').addEventListener('click', async function () {
  await fetch('/api/documents/42', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ html: editor.html.get() })
  });
});

// Load saved content later
editor.html.set('<p>Welcome back.</p>');
Caution
Treat editor HTML as user input. Sanitize it on your server with a maintained HTML sanitizer before you store or display it.

See a headless editor in action

The headless mode example combines everything in this guide into one working page. Try the custom toolbar in your browser, then copy the full HTML, CSS, and JavaScript into your project.

The example includes:

  • A toolbar built from data. Each button is one line in a list, so adding a button takes a single edit.

  • Pressed button states for bold, italic, headings, quotes, and lists.

  • An inline link form that keeps the user’s text selection while they type a URL.

  • Undo and redo that move one change at a time.

  • A live HTML output panel that shows what html.get() returns as you edit.

Open the headless mode example, or edit it in JSFiddle from the same page.

Use headless mode in React

The official React wrapper passes config straight to Froala, so headless: true works the same way. Install the core and the wrapper at matching versions so npm keeps a single copy of the editor.

BASH

npm install froala-editor@5.5.0 react-froala-wysiwyg@5.5.0

JSX

import { useMemo, useRef, useState } from 'react';
import FroalaEditorComponent from 'react-froala-wysiwyg';
import 'froala-editor/css/froala_editor.min.css';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/js/plugins.pkgd.min.js';

const BUTTONS = [
  { label: 'Bold',    run: (ed) => ed.commands.bold() },
  { label: 'Italic',  run: (ed) => ed.commands.italic() },
  { label: 'Bullets', run: (ed) => ed.lists.format('UL') },
  { label: 'Undo',    run: (ed) => ed.commands.undo(), history: true }
];

export default function HeadlessEditor() {
  const editorRef = useRef(null);
  const [ready, setReady] = useState(false);
  const [html, setHtml] = useState('<p>Start writing here.</p>');

  // A stable config object keeps the editor from re-initializing on every render.
  const config = useMemo(() => ({
    headless: true,
    events: {
      initialized: () => setReady(true)
    }
  }), []);

const handleClick = (button) => {
  const editor = editorRef.current && editorRef.current.getEditor();
  if (!editor) return;
  // Undo and redo move through the history, so they run directly.
  if (button.history) {
    button.run(editor);
    return;
  }
  // Save an undo step after each change so Undo and Redo can find it.
  button.run(editor);
  editor.undo.saveStep();
};

  return (
    <div>
      <div className="my-toolbar" role="toolbar" aria-label="Formatting">
        {BUTTONS.map((b) => (
          <button
            key={b.label}
            type="button"
            disabled={!ready}
            onMouseDown={(e) => e.preventDefault()}
            onClick={() => handleClick(b)}
          >
            {b.label}
          </button>
        ))}
      </div>

      <FroalaEditorComponent
        ref={editorRef}
        tag="div"
        config={config}
        model={html}
        onModelChange={setHtml}
      />
    </div>
  );
}
The ref gives you the wrapper, and getEditor() returns the Froala instance with the full Action API. onModelChange keeps the latest HTML in React state, ready to save.
Next.js
Froala runs in the browser. Load this component with dynamic(() => import('./HeadlessEditor'), { ssr: false }) so it renders on the client only.
Using Vue or Angular? Pass headless: true inside the wrapper’s config object in the same way. See the framework guides for each wrapper’s setup.

What changes in headless mode

Headless mode sorts the API into two groups. Content methods run as usual. Methods whose only job is to open Froala’s own UI return safely as no-ops, so leftover calls in shared code stay harmless.

  • Action API runs normally
  • UI API safe no-op
  • Defaults adjusted, still configurable
AREA HEADLESS BEHAVIOR
Toolbar Your app provides it. Froala’s toolbar stays detached from the page, and toolbar.show(), hide(), showInline(), disable(), and enable() are no-ops.
Popups and modals popups.show(), modals.show(), and plugin methods that open one (such as colors.showColorsPopup(), image.showAltPopup(), and table.showTableProperties()) are no-ops.
Toasts Always off.
Floating helpers Quick insert, line breaker, drag handles, and image resize handles are skipped entirely.
Word paste Pasted Word content is cleaned right away, skipping the keep or clean dialog. wordPasteKeepFormatting defaults to false and accepts your own value.
Counters charCounterCount and wordCounterCount default to false. Set either to true to turn it on.
Collaboration Stays inactive, even with collabConfig set. The collaborative.* action methods are no-ops.
AI features The AI Chat panel, inline AI suggestions, and AI Improve Writing sessions stay inactive.
Styling The fr-headless class resets Froala’s wrapper and content styling, so your CSS takes over.
Caution
Froala reads headless once, at initialization. To switch modes, call editor.destroy() and create a new instance with the new setting.

Reference

OPTION headless

Runs the editor as a pure editing engine for your own UI.

FIELD TYPE DESCRIPTION
headless boolean Optional. Default: false. Set to true at initialization.

METHODS editor.helpers.*

Helpers for checking headless state and building headless-aware plugins.

METHOD RETURNS DESCRIPTION
isHeadless() boolean true when the editor runs with headless: true.
isUserOpt(key) boolean true only when you passed the option explicitly, as opposed to a default.
headlessDefault(key, value) void In headless mode, sets editor.opts[key] to value unless you set that option yourself. Outside headless mode, it leaves options as they are.
guardHeadlessUI(api, names) boolean In headless mode, replaces each named method on api with a no-op that returns false, then returns true.
ensureHeadlessFocus() void In headless mode, focuses the editor when the selection sits outside it, so the browser places a caret in the content.

Make custom plugins headless-aware

If you write your own Froala plugins, these helpers let them behave like the built-in ones. This section is optional for most apps.

Guard methods that open your own UI

A plugin that opens a dialog outside Froala’s popup system should switch that method off in headless mode. guardHeadlessUI does this in one line, and its return value tells you whether to skip building the UI.

JAVASCRIPT

FroalaEditor.PLUGINS.myPicker = function (editor) {
  function open() {
    const dialog = document.createElement('dialog');
    dialog.textContent = 'Pick an item';
    document.body.appendChild(dialog);
    dialog.showModal();
  }

  const api = { open: open };
  editor.helpers.guardHeadlessUI(api, ['open']);

  return api;
};

Give actions a caret to work from

Call ensureHeadlessFocus() at the start of any action that reads the selection. It focuses the editor only when needed, and it does nothing outside headless mode, so the same plugin works in both modes.

JAVASCRIPT

FroalaEditor.PLUGINS.myHighlighter = function (editor) {
  function apply() {
    editor.helpers.ensureHeadlessFocus();
    editor.format.applyStyle('background-color', 'yellow');
    editor.undo.saveStep(); // one Undo removes the highlight
  }

  return { apply: apply };
};

// Call it from your own toolbar
editor.myHighlighter.apply();

Change a default while respecting user settings

headlessDefault sets a headless-friendly default and keeps any value the integrator passed in. Froala’s character counter, word counter, and Word paste plugins use this same approach.

JAVASCRIPT

FroalaEditor.PLUGINS.myStatusBar = function (editor) {
  function _init() {
    // Off by default in headless mode, unless the integrator turned it on.
    editor.helpers.headlessDefault('myStatusBarEnabled', false);
  }

  return { _init: _init };
};

Next steps

With your headless editor running, these pages help you add more actions to your toolbar.

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