Advanced

Custom Plugins

Extend the editor with your own toolbar buttons, keyboard shortcuts, renderers, and parsers. Plugins implement the EditorPlugin interface.

Plugin Structure

A plugin is a plain object implementing the EditorPlugin interface:

my-plugin.ts

import type { EditorPlugin, PluginContext } from '@synclineapi/mdx-editor';

const myPlugin: EditorPlugin = {
  // Unique identifier
  name: 'my-widget',

  // Toolbar buttons contributed by this plugin
  toolbarItems: [
    {
      id: 'insertWidget',
      label: 'Widget',
      icon: '<svg>...</svg>',
      tooltip: 'Insert Widget',
      shortcutLabel: 'Ctrl+Shift+W',
      action({ editor }) {
        editor.insertBlock('::widget\n\n::');
      },
      isActive({ editor }) {
        return editor.getCurrentLine().startsWith('::widget');
      },
    },
  ],

  // Keyboard shortcuts
  shortcuts: [
    {
      key: 'ctrl+shift+w',
      description: 'Insert Widget',
      action({ editor }) {
        editor.insertBlock('::widget\n\n::');
      },
    },
  ],

  // Custom markdown-to-HTML renderers for the preview pane
  renderers: [
    {
      name: 'widget',
      pattern: /^::widget\n([\s\S]*?)\n::$/gm,
      render(content) {
        return `<div class="smdx-widget">${content}</div>`;
      },
    },
  ],

  // CSS injected into the editor
  styles: `
    .smdx-widget {
      border: 2px dashed var(--smdx-primary);
      border-radius: var(--smdx-radius);
      padding: 1rem;
    }
  `,

  // Called once when the plugin is registered
  init(ctx: PluginContext) {
    console.log('Widget plugin initialized');
  },

  // Called when the plugin is removed
  destroy(ctx: PluginContext) {
    console.log('Widget plugin destroyed');
  },
};

export default myPlugin;

Registering a Plugin

Pass your plugin object in the plugins array alongside built-in plugin factories:

editor.ts

import { createEditor } from '@synclineapi/mdx-editor';
import '@synclineapi/mdx-editor/style.css';
import myPlugin from './my-plugin';

// createEditor() includes all 36 built-in plugins by default.
// Add your custom plugin alongside them:
const editor = createEditor({
  container: '#editor',
  plugins: [...allPlugins(), myPlugin],
});

// Or build a minimal editor with only specific plugins:
import {
  SynclineMDXEditor,
  headingPlugin,
  boldPlugin,
} from '@synclineapi/mdx-editor';

const editor = new SynclineMDXEditor({
  container: '#editor',
  plugins: [headingPlugin(), boldPlugin(), myPlugin],
});

Plugin Context

The PluginContext passed to init() and destroy() provides access to the editor and dynamic registration methods:

plugin-context.d.ts

// The PluginContext passed to init() and destroy():
interface PluginContext {
  editor: EditorAPI;
  registerToolbarItem(item: ToolbarItemConfig): void;
  registerShortcut(shortcut: ShortcutConfig): void;
  registerRenderer(renderer: RendererConfig): void;
  registerParser(parser: ParserConfig): void;
  injectStyles(css: string): void;
  emit(event: string, data?: unknown): void;
  on(event: string, handler: EventHandler): void;
  off(event: string, handler: EventHandler): void;
}

Editor API

Inside toolbar actions and via ctx.editor, you have access to the full editor API:

editor-api.d.ts

// EditorAPI — available inside toolbar actions and via ctx.editor:
interface EditorAPI {
  getValue(): string;
  setValue(value: string): void;
  insertText(text: string): void;
  wrapSelection(prefix: string, suffix: string): void;
  replaceSelection(text: string): void;
  getSelection(): SelectionState;
  setSelection(start: number, end: number): void;
  insertBlock(template: string): void;
  getTextarea(): HTMLTextAreaElement;
  getPreview(): HTMLElement;
  getRoot(): HTMLElement;
  focus(): void;
  renderPreview(): void;
  getMode(): EditorMode;
  setMode(mode: EditorMode): void;
  registerPlugin(plugin: EditorPlugin): void;
  unregisterPlugin(name: string): void;
  on(event: string, handler: EventHandler): void;
  off(event: string, handler: EventHandler): void;
  emit(event: string, data?: unknown): void;
  destroy(): void;
  undo(): void;
  redo(): void;
  getCurrentLine(): string;
  getCurrentLineNumber(): number;
  replaceCurrentLine(text: string): void;
  insertAt(position: number, text: string): void;
  getWordCount(): number;
  getLineCount(): number;
}

Dynamic Registration

Plugins can be registered or removed at runtime via the editor instance:

dynamic.ts

// Plugins can also be registered/unregistered at runtime:
const editor = createEditor({ container: '#editor' });

// Register later
editor.registerPlugin(myPlugin);

// Remove at runtime
editor.unregisterPlugin('my-widget');

EditorPlugin Reference

PropertyTypeRequiredDescription
namestringYes

Unique identifier for the plugin.

init(ctx: PluginContext) => void | Promise<void>No

Called once when the plugin is registered. Receives the PluginContext.

destroy(ctx: PluginContext) => voidNo

Called when the plugin is removed. Clean up resources here.

toolbarItemsToolbarItemConfig[]No

Toolbar buttons contributed by this plugin.

shortcutsShortcutConfig[]No

Keyboard shortcuts. Key combos like "ctrl+shift+w".

renderersRendererConfig[]No

Custom markdown-to-HTML renderers for the preview pane.

parsersParserConfig[]No

Custom block parsers for extended MDX syntax.

stylesstringNo

CSS string injected into the editor.

dependenciesstring[]No

Names of plugins that must be registered first.

SynclineMDX

© 2026 SynclineMDX Editor. All rights reserved.