Create a plugin

A Redactor plugin is a class that extends the editor: buttons, commands, events, optional custom blocks. This guide covers structure, loading, registration, blocks, and packaging as an IIFE bundle.

Plugin class#

Minimal plugin:

class GreetingPlugin {
    static defaultOptions = {
        icon: '<svg …>',
        prefix: 'Hello'
    };

    static defaultLang = {
        en: {
            greeting: {
                title: 'Greeting',
                text: 'Hello from plugin!'
            }
        }
    };

    constructor(app, options = {}) {
        this.app = app;
        this.options = options;
    }

    start() {
        this.app.toolbar.add('greeting', {
            title: '## greeting.title ##',
            icon: this.options.icon,
            command: 'greeting.insert',
            position: { after: 'add' }
        });
    }

    insert() {
        const inserter = this.app.getService('inserter');
        const prefix = this.options.prefix || '';
        const text = this.app.getLang('greeting.text');
        inserter.insertHtml(`<p>${prefix ? prefix + ': ' : ''}${text}</p>`, { caret: 'end' });
    }
}

Enable with overrides:

Redactor('#entry', {
    plugins: ['greeting'],
    greeting: {
        prefix: 'Hi there'
    }
});

Merged options are available as this.options in the plugin instance.

Lifecycle

Startup order:

  1. constructor(app, options) — plugin instance is created
  2. Editor DOM and toolbox are built; initial content is parsed
  3. start() — modules and plugins start (toolbar exists; use for buttons and UI)
  4. load() — after parser:loaded, when the document is mounted and editor:ready is about to fire
  5. stop() — on app.stop(), teardown
Method When
constructor(app, options) Plugin instantiated during editor init
start() After UI is built; register buttons, listen to events
load() After content is parsed and blocks are mounted — late setup that needs a ready document
stop() Editor shutdown

Most plugins only need start(). Use load() when you must run after the first parse (e.g. scan blocks in the document).

Static properties

Property Purpose
static defaultOptions Default plugin settings merged into this.options
static defaultLang i18n strings (## plugin.key ## in titles)

Access editor settings, modules, and services

From any plugin method, use this.app:

Editor options (global Redactor config):

const lang = this.app.getOption('lang');
const uploadUrl = this.app.getOption('image.upload');
const allOptions = this.app.getOptions();

if (this.app.isOption('image.upload')) {
    // image upload is enabled
}

getOption('dotted.path') reads nested keys. isOption treats false, null, and undefined as disabled.

Modules — feature APIs (toolbar, image, parser, dropdown, …):

const toolbar = this.app.getModule('toolbar');
// or shorthand:
this.app.toolbar.add('myBtn', { /* … */ });

this.app.image.insert({ url: '/photo.jpg' }, 'insert');
this.app.dropdown.close();

Services — internal helpers (inserter, transport, blockManager, …):

const inserter = this.app.getService('inserter');
inserter.insertHtml('<p>Text</p>', { caret: 'end' });

const transport = this.app.getService('transport');
transport.get({ url: '/api/data', responseType: 'json', success: (json) => { /* … */ } });

Other plugins:

if (this.app.isPluginActive('counter')) {
    this.app.counter.getCount();
}

See API in the docs for module and service reference lists.

Commands

A button command: 'greeting.insert' calls app.greeting.insert() — the plugin instance is exposed on app under the plugin key.

Same via invoke:

app.invoke('greeting.insert');

Plugin key must not match an existing core module name.

Register a plugin#

Global registration

Call before creating the editor:

Redactor.addPlugin('greeting', GreetingPlugin);

Redactor.addPlugin(key, PluginClass) stores the class in Redactor’s global registry. Multiple editors on one page share the same registration.

Enable on an editor

Script tags (IIFE) — load the plugin file after redactor.js, then list the key:

<script src="/assets/redactor/redactor.min.js"></script>
<script src="/assets/redactor/plugins/greeting/greeting.min.js"></script>
<script>
    Redactor('#entry', {
        plugins: ['greeting']
    });
</script>

Several plugins:

Redactor('#entry', {
    plugins: ['counter', 'emoji', 'greeting']
});

ES modules — import the ESM core, register your plugin class, enable by key:

import Redactor from './redactor.esm.min.js';
import { GreetingPlugin } from './GreetingPlugin.js'; // your plugin file

Redactor.addPlugin('greeting', GreetingPlugin);

Redactor('#entry', {
    plugins: ['greeting']
});

Plugin options

Merge order: defaultOptions → plugins.{key}.options → top-level {key}: { … }.

Redactor('#entry', {
    plugins: ['accordion'],
    accordion: {
        icon: '<svg …>'
    }
});

Or inline:

plugins: {
    accordion: {
        plugin: 'accordion',
        options: { icon: '<svg …>' }
    }
}

Plugins with custom blocks#

Some plugins add new block types (accordion, mention, math). Register blocks before the plugin:

Redactor.addBlock('accordion', AccordionBlock);
Redactor.addBlock('accordion-item', AccordionItemBlock);
Redactor.addPlugin('accordion', AccordionPlugin);

At editor startup, blocks from Redactor.addBlock() are registered in BlockManager alongside built-in blocks.

Block class basics

Extend Redactor.Block for block-level nodes, or Redactor.InlineBlock for inline tokens (mentions, merge tags).

class AccordionBlock extends Redactor.Block {
    static meta = {
        type: 'accordion',
        label: 'Accordion',
        nested: true,
        canBeParent: true,
        focusable: true,
        editable: false,
        preserveDataBlock: true
    };

    static match(el) {
        return el?.dataset?.block === 'accordion';
    }

    createEl() {
        const doc = this.app.host.getDocument();
        return doc.createElement('div');
    }

    init(data) {
        // apply data when created from JSON or API
    }

    getData() {
        return { ...super.getData() /* type-specific fields */ };
    }
}

Important pieces:

Piece Role
static meta Type id, nesting, UI overrides, inline: true for inline blocks
static match(el) Detect existing HTML when parsing content
static shouldSkip(el, { phase }) Skip child elements so they are not parsed as separate blocks
createEl() DOM for a new block
init(data) Apply data from JSON / block.create()
getData() Export block fields for getJson()

Nested blocks (accordion)

Parent block (nested: true, canBeParent: true) holds children. Child blocks use skipNested: true so inner headings/paragraphs stay inside the parent.

The plugin inserts structure programmatically:

insert() {
    const accordion = this.app.block.create('accordion');
    const item = this.app.block.create('accordion-item', { open: true });
    accordion.getEl().appendChild(item.getEl());

    const inserter = this.app.getService('inserter');
    inserter.insertBlock(accordion);

    this.app.blocks.mountBlocksInSubtree(accordion.getEl());
}

Control-dropdown buttons can be declared in block meta.ui.dropdowns (see AccordionItemBlock in the codebase).

Inline blocks (mention)

Inline blocks live inside text (Redactor.InlineBlock, meta.inline: true):

class MentionBlock extends Redactor.InlineBlock {
    static meta = {
        type: 'mention',
        inline: true,
        editable: false,
        focusable: true,
        preserveDataBlock: true
    };

    static match(el) {
        return el.tagName === 'SPAN' && el.dataset.block === 'mention';
    }

    init(data = {}) {
        this.el.dataset.block = 'mention';
        this.el.textContent = data.content ?? '';
        if (data.userId) this.el.dataset.userId = data.userId;
    }
}

The mention plugin registers a trigger (@) and creates blocks via app.block.create('mention', { … }) when the user picks an item — no toolbar button required.

Override without global registration

Pass block classes in editor options (per instance):

Redactor('#entry', {
    blocks: {
        myCard: MyCardBlock
    }
});

Same as Redactor.addBlock but scoped to one editor.

Events and hooks#

Subscribe in the constructor or start():

constructor(app) {
    this.app = app;
    this.app.on('output:before:html', (event) => {
        // return { html } to modify export
        return event;
    });
}

Hook handlers may return a modified payload. Signal events (blocks:change, mention:insert) are notify-only.

IIFE bundle#

Ship plugins as a separate file that does not pollute the global scope. Wrap everything and pass Redactor in:

(function () {

class GreetingPlugin {
    // …
}

Redactor.addPlugin('greeting', GreetingPlugin);

})(Redactor);

Rules:

  1. Load redactor.js (or redactor.min.js) first — the IIFE receives the global Redactor constructor.
  2. Do not use import/export inside the IIFE file — inline classes or a bundler that outputs IIFE.
  3. Call Redactor.addPlugin (and Redactor.addBlock if needed) inside the wrapper.
  4. Avoid leaking variables — const/class inside the function stay private.

IIFE with blocks

Plugins with blocks read base classes from Redactor:

(function () {

    const Block = globalThis.Redactor?.Block;

    if (!Block) {
        throw new Error('[My plugin] Load redactor.js before this bundle.');
    }

    class AccordionBlock extends Block {
        static meta = { type: 'accordion', /* … */ };
        // …
    }

    class AccordionPlugin {
    // …
    }

    Redactor.addBlock('accordion', AccordionBlock);
    Redactor.addPlugin('accordion', AccordionPlugin);

})(Redactor);

For inline blocks use Redactor.InlineBlock. Built-in bases exported on Redactor: Block, InlineBlock, CommonBlock.

Plugin-only IIFE (no blocks)

Same pattern as the anchor plugin — only the plugin class:

(function () {

    class AnchorPlugin {
        start() {
            this.app.toolbar.add('anchor', { /* … */ });
        }
        // …
    }

    Redactor.addPlugin('anchor', AnchorPlugin);

})(Redactor);

Build tips

  • Name the output file after the plugin key, e.g. plugins/greeting/greeting.min.js.
  • One plugin per IIFE file keeps load order predictable.
  • If you use a bundler (esbuild, Rollup), set format: 'iife' and wrap with (function () { … })(Redactor);.
  • In ESM apps, import redactor.esm.min.js, set globalThis.Redactor, load your IIFE plugin script, then enable it with plugins: ['greeting'].

Built plugins ship only as files under plugins/ — not as an importable class tree.