If you’ve ever used Microsoft Word or Google Docs, you’re familiar with the Format Painter tool — that magical brush icon that lets you copy formatting from one text block and apply it to another with a single click. It’s one of those features that seems simple on the surface but dramatically improves editing efficiency.
Today, we’re going to build this exact functionality for Froala Editor, and in doing so, reveal the elegant architecture that makes Froala one of the most extensible WYSIWYG editors available.
Why This Matters: The Power of Custom Plugins
Froala Editor ships with an impressive array of features out of the box, but the real magic lies in its modular plugin architecture. Unlike monolithic editors that force you to work within rigid constraints, Froala treats extensibility as a first-class citizen.
This architectural decision means you’re never locked into a predetermined feature set. Need a custom workflow? Build a plugin.
The Paint Format feature we’re building today demonstrates several critical aspects of Froala’s architecture:
Clean API design that separates concerns between UI, state management, and content manipulation
Event-driven architecture that hooks into the editor’s lifecycle
Format API that abstracts away DOM complexity
Undo/redo integration that maintains editing history integrity
Toolbar integration that provides visual feedback and state management
Let’s dive into the code and see how these pieces fit together.
The Complete Paint Format Plugin: Architecture Overview
Our Paint Format plugin follows Froala’s standard plugin pattern, which consists of three main components:
Icon Definition — Registers the visual representation in the toolbar
Command Registration — Connects the toolbar button to plugin logic
Plugin Implementation — Contains the core functionality and state management
Here’s the complete implementation with detailed explanations:
// ============================================================
// FORMAT PAINTER (Paint Format) PLUGIN FOR FROALA V5
// ============================================================
(function (FroalaEditor) {
// ------------------------------------------------------------
// 1️⃣ Define Toolbar Icon
// ------------------------------------------------------------
FroalaEditor.DefineIcon("paintFormat", {
template: "svgMultiplePath",
PATHS: ``,
VIEWBOX: "0 0 1024 1024",
});
// ------------------------------------------------------------
// 2️⃣ Register Toolbar Command
// ------------------------------------------------------------
FroalaEditor.RegisterCommand('paintFormat', {
title: 'Paint Format',
focus: false,
undo: true,
refreshAfterCallback: false,
callback: function () {
this.paintFormat.toggle();
},
refresh: function ($btn) {
if (this.paintFormat.isActive()) {
$btn.addClass('fr-active');
} else {
$btn.removeClass('fr-active');
}
}
});
// ------------------------------------------------------------
// 3️⃣ Plugin Definition
// ------------------------------------------------------------
FroalaEditor.PLUGINS.paintFormat = function (editor) {
let active = false;
let storedFormats = null;
// ==========================================================
// CAPTURE FORMATTING FROM CURRENT SELECTION
// ==========================================================
function captureFormats() {
const element = editor.selection.element();
if (!element) return;
const computed = window.getComputedStyle(element);
storedFormats = {
bold: editor.format.is('strong'),
italic: editor.format.is('em'),
underline: editor.format.is('u'),
strike: editor.format.is('strike'),
fontSize: computed.fontSize,
color: computed.color,
backgroundColor: computed.backgroundColor,
fontFamily: computed.fontFamily,
textAlign: computed.textAlign
};
}
// ==========================================================
// APPLY STORED FORMATTING TO NEW SELECTION
// ==========================================================
function applyFormats() {
if (!storedFormats) return;
editor.undo.saveStep();
if (storedFormats.bold) editor.format.apply('strong');
if (storedFormats.italic) editor.format.apply('em');
if (storedFormats.underline) editor.format.apply('u');
if (storedFormats.strike) editor.format.apply('strike');
if (storedFormats.fontSize) {
editor.format.applyStyle('font-size', storedFormats.fontSize);
}
if (storedFormats.color) {
editor.format.applyStyle('color', storedFormats.color);
}
if (
storedFormats.backgroundColor &&
storedFormats.backgroundColor !== 'rgba(0, 0, 0, 0)'
) {
editor.format.applyStyle(
'background-color',
storedFormats.backgroundColor
);
}
if (storedFormats.fontFamily) {
editor.format.applyStyle('font-family', storedFormats.fontFamily);
}
if (storedFormats.textAlign) {
const blocks = editor.selection.blocks();
if (blocks && blocks.length) {
blocks.forEach(block => {
block.style.textAlign = storedFormats.textAlign;
});
}
}
editor.undo.saveStep();
}
// ==========================================================
// TOGGLE PAINTER MODE
// ==========================================================
function toggle() {
if (!active) {
captureFormats();
if (!storedFormats) return;
active = true;
} else {
active = false;
}
editor.toolbar.refresh();
}
function isActive() {
return active;
}
// ==========================================================
// EVENT BINDINGS
// ==========================================================
function bindEvents() {
editor.events.on('mouseup', function () {
if (!active) return;
setTimeout(function () {
if (editor.selection.isCollapsed()) return;
applyFormats();
}, 0);
});
editor.events.on('keydown', function (e) {
if (e.key === 'Escape' && active) {
active = false;
editor.toolbar.refresh();
}
});
}
function _init() {
bindEvents();
}
return {
_init: _init,
toggle: toggle,
isActive: isActive
};
};
})(FroalaEditor);
Breaking Down the Architecture
Want to see this in action? Try the Paint Format plugin live on JSFiddle — click the paint brush icon, select some text with different formatting, then paint that style onto other content. It’s the fastest way to understand how all these pieces work together in real time.
Icon Definition: Visual Identity
The first step in creating any toolbar-based plugin is defining its visual representation. Froala’s DefineIcon method provides a flexible system that supports both FontAwesome icons and custom SVG graphics:
FroalaEditor.DefineIcon("paintFormat", {
template: "svgMultiplePath",
PATHS: ``,
VIEWBOX: "0 0 1024 1024",
});
The template: "svgMultiplePath" property instructs Froala to render this as an inline SVG graphic, giving you complete control over the icon’s visual appearance. The PATHS property contains the full SVG path data that defines the shape of the paint bucket icon—a detailed vector graphic that scales perfectly at any size. The VIEWBOX: "0 0 1024 1024" establishes the coordinate system for the SVG, defining a 1024×1024 viewbox that ensures consistent rendering. By using inline SVG instead of external icon libraries or font icons, you eliminate dependencies on third-party icon sets and gain complete flexibility over styling, animation, and appearance. The icon identifier ('paintFormat') becomes the namespace for your entire plugin—a convention that keeps the codebase organized and prevents naming collisions.
Command Registration: Bridging UI and Logic
The RegisterCommand method is where UI meets functionality. This is Froala’s way of creating a clean separation between presentation and business logic:
FroalaEditor.RegisterCommand('paintFormat', {
title: 'Paint Format',
focus: false,
undo: true,
refreshAfterCallback: false,
callback: function () {
this.paintFormat.toggle();
},
refresh: function ($btn) {
if (this.paintFormat.isActive()) {
$btn.addClass('fr-active');
} else {
$btn.removeClass('fr-active');
}
}
});
The callback function executes when the user clicks the toolbar button. Notice how it delegates to this.paintFormat.toggle()—this is Froala’s plugin system in action. The this context refers to the editor instance, and paintFormat is automatically available because we’re defining it in the PLUGINS namespace.
The refresh function is called whenever the toolbar updates, allowing us to maintain visual state. By adding or removing the fr-active class, we create that satisfying toggle effect where the button stays highlighted while Paint Format mode is active.
Plugin Core: State Management and Format Manipulation
The heart of our plugin lives in the PLUGINS.paintFormat function. This follows the revealing module pattern, exposing only the methods that need to be public while keeping internal state private:
FroalaEditor.PLUGINS.paintFormat = function (editor) {
let active = false;
let storedFormats = null;
// ... implementation
return {
_init: _init,
toggle: toggle,
isActive: isActive
};
};
The editor parameter gives us access to Froala’s entire API surface. The two state variables—active and storedFormats—maintain the plugin’s operational state across user interactions.
Capturing Formats: Reading the DOM Intelligently
The captureFormats function demonstrates Froala’s hybrid approach to content manipulation—combining high-level API calls with direct browser APIs when appropriate:
function captureFormats() {
const element = editor.selection.element();
if (!element) return;
const computed = window.getComputedStyle(element);
storedFormats = {
bold: editor.format.is('strong'),
italic: editor.format.is('em'),
underline: editor.format.is('u'),
strike: editor.format.is('strike'),
fontSize: computed.fontSize,
color: computed.color,
backgroundColor: computed.backgroundColor,
fontFamily: computed.fontFamily,
textAlign: computed.textAlign
};
}
Notice the strategic use of two different APIs:
Froala’s Format API (
editor.format.is()) for semantic HTML elements like and . This approach respects Froala’s internal representation and ensures compatibility with other plugins.Browser’s Computed Style API (
window.getComputedStyle()) for CSS properties like font size and color. This captures the actual rendered appearance, regardless of how it was applied (inline styles, CSS classes, or inherited styles).
This hybrid approach is crucial. If we only used DOM inspection, we’d miss semantic formatting. If we only used Froala’s API, we’d miss CSS-based styling. By combining both, we capture the complete formatting picture.
Applying Formats: Writing Changes Safely
The applyFormats function is where we modify the document. This is also where Froala’s architecture really shines:
function applyFormats() {
if (!storedFormats) return;
editor.undo.saveStep();
if (storedFormats.bold) editor.format.apply('strong');
if (storedFormats.italic) editor.format.apply('em');
if (storedFormats.underline) editor.format.apply('u');
if (storedFormats.strike) editor.format.apply('strike');
if (storedFormats.fontSize) {
editor.format.applyStyle('font-size', storedFormats.fontSize);
}
// ... more formatting applications
editor.undo.saveStep();
}
The editor.undo.saveStep() calls bookend our changes, creating a single undo point for the entire format application. This is critical for user experience—users expect one undo to reverse the entire Paint Format operation, not each individual style change.
The editor.format.apply() and editor.format.applyStyle() methods handle all the complexity of:
Splitting text nodes at selection boundaries
Wrapping content in appropriate HTML elements
Merging adjacent identical formatting
Maintaining proper nesting hierarchy
Preserving the selection after changes
If you tried to implement this with raw DOM manipulation, you’d need hundreds of lines of code and countless edge case handlers. Froala’s Format API abstracts all of this away.
Event Handling: Responding to User Actions
The bindEvents function wires up our plugin to the editor’s event system:
function bindEvents() {
editor.events.on('mouseup', function () {
if (!active) return;
setTimeout(function () {
if (editor.selection.isCollapsed()) return;
applyFormats();
}, 0);
});
editor.events.on('keydown', function (e) {
if (e.key === 'Escape' && active) {
active = false;
editor.toolbar.refresh();
}
});
}
The mouseup event handler is where the magic happens. When Paint Format mode is active and the user releases the mouse button (completing a selection), we apply the stored formatting. The setTimeout with zero delay is a common pattern that ensures the browser has updated the selection before we read it.
The keydown handler provides an escape hatch (literally)—pressing ESC deactivates Paint Format mode, giving users a quick way to cancel the operation without clicking the toolbar button again.
Integration: Adding the Plugin to Your Editor
Once you’ve created your plugin file (let’s call it froala-paint-format.js), integration is straightforward:
<!-- Include Froala core -->
<script src="froala_editor.min.js"></script>
<!-- Include your custom plugin -->
<script src="froala-paint-format.js"></script>
<script>
new FroalaEditor('#editor', {
toolbarButtons: [
'bold', 'italic', 'underline',
'|',
'paintFormat', // Your custom button
'|',
'formatOL', 'formatUL'
]
});
</script>
That’s it. No complex configuration, no build steps, no framework-specific adapters. The plugin automatically registers itself when the script loads, and you simply reference it by name in your toolbar configuration.
Why This Architecture Matters
The Paint Format plugin we’ve built demonstrates several architectural principles that make Froala exceptional for enterprise development:
1. Separation of Concerns: UI definition, command logic, and core functionality are cleanly separated, making the code maintainable and testable.
2. API-First Design: Instead of manipulating the DOM directly, we use Froala’s high-level APIs. This ensures our plugin remains compatible across editor versions and doesn’t conflict with other plugins.
3. Event-Driven Architecture: By hooking into the editor’s event system rather than polling or using timers, our plugin is efficient and responsive.
4. State Management: The plugin maintains its own state (active, storedFormats) without polluting the global scope or the editor’s internal state.
5. User Experience Focus: Features like the toggle button state, ESC key handling, and undo integration show attention to the details that make software feel polished.
Extending Further: Ideas for Enhancement
The beauty of Froala’s plugin system is that it’s infinitely extensible. Here are some ways you could enhance this Paint Format plugin:
Multiple Format Storage: Allow users to store multiple format “presets” and switch between them
Format Library: Create a dropdown that shows recently used formats
Selective Format Application: Add a modal that lets users choose which aspects of formatting to apply (colors only, fonts only, etc.)
Cross-Editor Sync: Store formats in localStorage to persist across page reloads or even different editor instances
Keyboard Shortcuts: Add hotkey support for power users (Ctrl+Shift+C to copy format, Ctrl+Shift+V to paste)
Each of these enhancements would follow the same architectural patterns we’ve explored today, demonstrating how Froala’s design scales from simple plugins to complex feature sets.
Conclusion: The Power of Extensibility
Building a custom Paint Format plugin for Froala Editor reveals something profound about the editor’s architecture: it’s not just a tool for editing content — it’s a platform for building editing experiences. The same APIs we used to build this plugin power every feature in Froala, from basic text formatting to complex table manipulation.
This architectural consistency means that as you master Froala’s plugin system, you’re not just learning how to add one feature — you’re learning how to add any feature. Need custom validation? Build a plugin. Want to integrate with your company’s design system? Build a plugin. Need to support a proprietary content format? You know what to do.
The modular architecture also means you’re never locked in.
This article was published on the Froala blog.

