Days
Hours
Minutes
Seconds
x
Skip to content
Froala Documentation

Build Your Own UI with Headless Mode

Headless mode keeps Froala’s editing engine and hands the interface to you.
You design the toolbar, and each button calls one Froala API method.

Headless Mode

NEW IN 5.5

Set the headless option to true and Froala renders a clean editable area. The built-in toolbar, popups and default editor styling step aside, so your app's own design system shapes the whole experience.

Try it yourself:


Edit in JSFiddle

HTML

<!-- Froala styles for saved content -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/css/froala_style.min.css">

<!-- Your toolbar is built into this element by JavaScript -->
<div id="my-toolbar" class="my-toolbar"></div>

<!-- A small inline form for adding links -->
<div id="link-row" class="link-row" hidden>
  <input id="link-url" type="url" placeholder="https://froala.com">
  <button type="button" id="link-apply" class="tb-btn is-primary">Insert link</button>
  <button type="button" id="link-cancel" class="tb-btn">Cancel</button>
</div>

<!-- The editor itself: an editable area -->
<div id="editor">
  <p>Select some text, then try the buttons above.</p>
</div>

<pre id="output" class="output"></pre>

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

CSS

/* Custom toolbar */
.my-toolbar { display: flex; flex-direction: column; gap: 10px; background: #fff;
  border: 1px solid #E7E9EE; border-radius: 12px 12px 0 0; padding: 12px; }

.tb-group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

.tb-label { font-size: 12px; font-weight: 700; color: #5F6773; width: 70px; flex-shrink: 0; }

.tb-btn { font: inherit; font-size: 13px; padding: 6px 10px; border-radius: 6px;
  border: 1px solid #DFE2E8; background: #F7F8FA; color: #2D313C; cursor: pointer; }

.tb-btn:hover { background: #E5F4FD; border-color: #0098F7; }

.tb-btn.is-active,
.tb-btn.is-primary { background: #0272B8; border-color: #0272B8; color: #fff; }

/* Link input row (replaces the browser prompt) */
.link-row { display: flex; gap: 6px; padding: 10px 12px; background: #F1F9FE;
  border: 1px solid #E7E9EE; border-top: none; }

.link-row[hidden] { display: none; }

.link-row input { flex: 1; min-width: 0; font: inherit; font-size: 14px; padding: 6px 10px;
  border: 1px solid #DFE2E8; border-radius: 6px; }

/* In headless mode, your CSS decides how the editing area looks */
#editor { background: #fff; border: 1px solid #E7E9EE; border-top: none; border-radius: 0 0 12px 12px; }

#editor .fr-element { padding: 20px 24px; min-height: 240px; font-size: 16px; line-height: 1.6; outline: none; }

#editor .fr-element:focus { box-shadow: inset 0 0 0 2px #B3DFFC; border-radius: 0 0 12px 12px; }

#editor table td { border: 1px solid #DFE2E8; padding: 6px 8px; }

#editor blockquote { border-left: 4px solid #15B69B; margin-left: 0; padding-left: 12px; color: #5F6773; }

/* Output panel */
.out-panel { margin-top: 14px; border: 1px solid #E7E9EE; border-radius: 12px; overflow: hidden; }

.out-head { font-size: 13px; font-weight: 700; padding: 9px 14px; background: #F0F1F3; color: #2D313C; }

.output { margin: 0; padding: 12px 14px; max-height: 200px; overflow: auto; background: #F8F8F8;
  font-size: 12.5px; white-space: pre-wrap; word-break: break-word; }

JAVASCRIPT

const outputEl = document.getElementById('output');
const linkRow = document.getElementById('link-row');
const linkInput = document.getElementById('link-url');

// Print the editor's current HTML into the output panel
function showHTML(editor) {
  outputEl.textContent = editor.html.get();
}

// 1. Create the editor in headless mode
const editor = new FroalaEditor('#editor', {
  // key: 'YOUR_LICENSE_KEY',   // add your license key here
  headless: true,               // keep the engine, bring your own UI
  htmlExecuteScripts: false,    // treat pasted scripts as plain content
  placeholderText: 'Start typing...',
  events: {
    initialized: function () {
      showHTML(this);
      buildToolbar(this);
    },
    contentChanged: function () {
      showHTML(this);
    }
  }
});

// 2. Your own buttons. Each one calls a Froala API method.
const GROUPS = [
  { label: 'Text', buttons: [
    { label: 'Bold', active: 'strong,b', run: e => e.commands.bold() },
    { label: 'Italic', active: 'em,i', run: e => e.commands.italic() },
    { label: 'Underline', active: 'u', run: e => e.commands.underline() },
    { label: 'Strike', active: 's,strike', run: e => e.commands.strikeThrough() },
    { label: 'Clear format', run: e => e.commands.clearFormatting() }
  ]},
  { label: 'Blocks', buttons: [
    { label: 'Paragraph', run: e => e.paragraphFormat.apply('N') },
    { label: 'H2', active: 'h2', run: e => e.paragraphFormat.apply('H2') },
    { label: 'H3', active: 'h3', run: e => e.paragraphFormat.apply('H3') },
    { label: 'Quote', active: 'blockquote', run: e => e.quote.apply('increase') }
  ]},
  { label: 'Layout', buttons: [
    { label: 'Bullets', active: 'ul', run: e => e.lists.format('UL') },
    { label: 'Numbers', active: 'ol', run: e => e.lists.format('OL') },
    { label: 'Left', run: e => e.align.apply('left') },
    { label: 'Center', run: e => e.align.apply('center') }
  ]},
  { label: 'Insert', buttons: [
    { label: 'Link', run: e => openLinkRow(e) },
    { label: 'Line', run: e => e.commands.insertHR() },
    { label: 'Table 2x2', run: e => e.table.insert(2, 2) }
  ]},
  { label: 'History', buttons: [
    { label: 'Undo', run: e => e.commands.undo() },
    { label: 'Redo', run: e => e.commands.redo() }
  ]}
];

// 3. Turn the GROUPS list into real <button> elements
function buildToolbar(ed) {
  const toolbar = document.getElementById('my-toolbar');
  const stateful = [];

  GROUPS.forEach(group => {
    const row = document.createElement('div');
    row.className = 'tb-group';
    row.innerHTML = '<span class="tb-label">' + group.label + '</span>';

    group.buttons.forEach(def => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'tb-btn';
      btn.textContent = def.label;

      // Keep the text selection when the button is pressed
      btn.addEventListener('mousedown', ev => ev.preventDefault());

      btn.addEventListener('click', () => {
        def.run(ed);
        ed.undo.saveStep();
        showHTML(ed);
        refresh();
      });

      row.appendChild(btn);

      if (def.active) {
        stateful.push({ btn, selector: def.active });
      }
    });

    toolbar.appendChild(row);
  });

  // Highlight buttons that match the text under the cursor
  function refresh() {
    const el = ed.selection.element();

    stateful.forEach(({ btn, selector }) => {
      const on = !!(
        el &&
        el.closest &&
        el.closest(selector) &&
        ed.el.contains(el.closest(selector))
      );

      btn.classList.toggle('is-active', on);
    });
  }

  ed.events.on('click', refresh);
  ed.events.on('keyup', refresh);
}

// 4. A small inline form for links (your UI, your rules)
function openLinkRow(ed) {
  ed.selection.save();
  linkRow.hidden = false;
  linkInput.value = '';
  linkInput.focus();

  document.getElementById('link-apply').onclick = () => {
    const url = linkInput.value.trim();

    ed.selection.restore();

    if (url) {
      ed.link.insert(url, ed.selection.text() || url);
    }

    ed.undo.saveStep();
    showHTML(ed);
    linkRow.hidden = true;
  };

  document.getElementById('link-cancel').onclick = () => {
    ed.selection.restore();
    linkRow.hidden = true;
  };
}

How It Works

  1. Create the editor with headless: true. Froala turns #editor into an editable area.
  2. Wait for initialized before building the toolbar. Editor methods are ready from this moment on.
  3. Describe buttons as data. The GROUPS list pairs a label with a function, for example e => e.commands.bold(). Adding a button means adding one line.
  4. Keep the selection. Calling preventDefault() on mousedown keeps the highlighted text selected while the user clicks your button.
  5. Read the result with editor.html.get() inside the contentChanged event.

API methods used

commands.bold() Also italic(), underline(), strikeThrough(), clearFormatting(), insertHR(), undo(), redo().
paragraphFormat.apply() Sets the block type. Accepts 'N' (normal), 'H2', 'H3' and more.
quote.apply() Pass 'increase' to wrap blocks in a blockquote.
lists.format() Pass 'UL' for bullets or 'OL' for numbers.
align.apply() 'left', 'center', 'right' or 'justify'.
link.insert() Inserts a link with (href, text).
table.insert() Inserts a table with (rows, columns).
html.get() Returns the current content as an HTML string.

Learn the full setup

This example shows headless mode in one working page. The headless editor setup guide explains each piece step by step and covers what a production app needs.

  • React setup with the official Froala wrapper.
  • Styling the editing area with your own CSS.
  • Saving and loading content with html.get() and html.set().
  • Everything that changes when headless is set to true.
  • Helper methods for making your own plugins work in headless mode.

Read the headless editor setup guide