New Froala Editor v5.5.0 is here – Learn More
Froala Documentation
- Installation Guides
- Browser Support
- Languages Support
- Shortcuts
- Activation
- Examples
- Customize the Editor
- Use-cases
- Plugins
- APIs
- Development Frameworks
- Server Integrations
- Server SDKs
- Migration Guides
- Changelog
- Tutorials
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.5Set 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:
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
- Create the editor with
headless: true. Froala turns#editorinto an editable area. - Wait for
initializedbefore building the toolbar. Editor methods are ready from this moment on. - Describe buttons as data. The
GROUPSlist pairs a label with a function, for examplee => e.commands.bold(). Adding a button means adding one line. - Keep the selection. Calling
preventDefault()onmousedownkeeps the highlighted text selected while the user clicks your button. - Read the result with
editor.html.get()inside thecontentChangedevent.
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()andhtml.set(). - Everything that changes when
headlessis set totrue. - Helper methods for making your own plugins work in headless mode.
Read the headless editor setup guide