Skip to main content
Lumen Editor’s plugin system lets you extend the editor with your own behavior — status bars, word counts, character limits, custom keyboard shortcuts, analytics hooks, and more — without forking or monkey-patching the core library. A plugin is just a plain function, so there’s nothing new to learn: if you can write JavaScript, you can write a plugin.

Plugin shape

A plugin is a function that receives the editor instance and returns nothing:
(editor: Editor) => void
That’s the entire contract. You can read from the editor, write to the DOM, subscribe to events, or call any public editor method — all from within that single function. There are no lifecycle hooks, no class to extend, and no registration API beyond passing your function to the plugins option.
Plugins run after the editor has fully mounted and its DOM is ready. This means editor.root is available and all built-in modules have already been initialized when your plugin function executes.

Full example: word count plugin

The following plugin appends a live word-count display beneath the editor and updates it on every change:
function wordCount(editor) {
  // Create and append the display element
  const el = document.createElement('div');
  el.className = 'wc';
  editor.root.appendChild(el);

  // Update on every content change
  editor.on('change', () => {
    const words = editor
      .getText()
      .trim()
      .split(/\s+/)
      .filter(Boolean).length;

    el.textContent = words + ' words';
  });
}

const editor = new Editor('#e', {
  plugins: [wordCount]
});

Accessing the DOM

Inside your plugin, editor.root is the live contenteditable element that the editor manages. You can append child elements to it, add event listeners, or read its innerHTML and textContent:
function highlightEmpty(editor) {
  editor.on('change', () => {
    if (!editor.getText().trim()) {
      editor.root.classList.add('is-empty');
    } else {
      editor.root.classList.remove('is-empty');
    }
  });
}
Avoid mutating editor.root.innerHTML directly — doing so bypasses the editor’s internal state and history stack, which can lead to inconsistent undo/redo behavior. Use the editor’s public API methods instead when you need to change content programmatically.

Subscribing to events

Use editor.on(eventName, handler) inside your plugin to react to editor events. All of the editor’s built-in events are available:
function analyticsPlugin(editor) {
  editor.on('change', (html) => {
    analytics.track('editor:change', { length: html.length });
  });

  editor.on('autosave', () => {
    analytics.track('editor:autosave');
  });

  editor.on('error', ({ code, message }) => {
    analytics.track('editor:error', { code, message });
  });
}
EventPayloadWhen it fires
changehtml: stringEvery content change
autosaveContent saved to localStorage
error{ code, message }Any editor error

Installing plugins

You can install plugins in two ways:
1

Via the plugins option at initialization

Pass an array of plugin functions to the plugins option. All plugins in the array run once, in order, after the editor mounts:
const editor = new Editor('#editor', {
  plugins: [wordCount, analyticsPlugin, highlightEmpty]
});
2

Via editor.use() after initialization

Call editor.use(plugin) at any point after the editor has been constructed. The plugin function runs immediately:
const editor = new Editor('#editor', { /* ... */ });

// Install a plugin later, e.g. after lazy-loading it
import('./plugins/spellcheck.js').then(({ spellcheck }) => {
  editor.use(spellcheck);
});
For reusable plugins that accept configuration, wrap them in a factory function that returns the (editor) => void function. For example: createWordCount({ className: 'my-wc' }) returns a configured plugin function you can pass directly to the plugins array.