Advanced

Custom Plugins

Extend the editor with your own toolbar buttons, keyboard shortcuts, patterns, components, 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';
import { componentTagTokens } 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::');
      },
    },
  ],

  // Raw regex-based pattern renderers — for non-JSX fenced syntax only
  // (e.g. fenced code blocks, :::type admonition fences, or $...$ math).
  patterns: [
    {
      name: 'widget',
      pattern: /^::widget\n([\s\S]*?)\n::$/gm,
      render(content) {
        return `<div class="smdx-widget">${content}</div>`;
      },
    },
  ],
  // Declarative JSX component definitions — the preferred way for JSX-style MDX.
  // Pass the return value of defineComponent() for each component.
  // components: [
  //   defineComponent({
  //     tag: 'Widget',
  //     attrs: { title: { type: 'string', default: '' } },
  //     render: ({ title, children }) =>
  //       `<div class="smdx-widget"><h4>${title}</h4>${children}</div>`,
  //   }),
  // ],
  // Autocomplete items contributed by this plugin.
  // 'cls' = component name, 'snip' = snippet with $1 tab-stop cursor.
  completions: [
    { label: 'Widget', kind: 'cls', detail: '<Widget> custom component' },
    {
      label: 'widget',
      kind: 'snip',
      detail: 'Insert Widget block',
      body: '::widget\n$1\n::',
    },
  ],

  // Per-line syntax highlighter. Return TokenSegment[] to colour your MDX syntax.
  // Use componentTagTokens() for JSX-style components — it handles:
  //   tag names, value attrs, boolean attrs, and multi-line tags automatically.
  provideTokens: componentTagTokens(['Widget']),
  // Or write a fully custom provider:
  // provideTokens: (line) => {
  //   const segs = [];
  //   const m = line.match(/^(::)(widget)/);
  //   if (m) {
  //     segs.push({ cls: 'kw',  start: 0, end: 2 });
  //     segs.push({ cls: 'cls', start: 2, end: 2 + m[2].length });
  //   }
  //   return segs;
  // },

  // 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;
JSX Components

JSX Component Plugin — components + defineComponent()

For JSX-style MDX tags, use components with defineComponent(). It gives you strictly-typed props, automatic preview rendering, syntax highlighting, and autocomplete — all from a single declaration. No regex required.

badge-plugin.ts

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

const badgePlugin: EditorPlugin = {
  name: 'badge',

  // Toolbar button to insert the component
  toolbarItems: [
    {
      id: 'insertBadge',
      label: 'Badge',
      icon: '<svg>...</svg>',
      tooltip: 'Insert Badge',
      action({ editor }) {
        editor.insertBlock('<Badge type="info" label="Badge" />');
      },
    },
  ],

  // defineComponent() handles attribute parsing, preview rendering,
  // syntax-highlight tokens, and autocomplete — all in one declaration.
  components: [
    defineComponent({
      tag: 'Badge',
      description: 'Inline status badge',
      attrs: {
        type: {
          type: 'enum',
          options: ['info', 'success', 'warning', 'error'],
          default: 'info',
        },
        label: { type: 'string', default: 'Badge' },
      },
      // TypeScript infers the exact prop types from attrs — no manual annotations.
      // type  → 'info' | 'success' | 'warning' | 'error'
      // label → string
      render: ({ type, label }) =>
        `<span class="smdx-badge smdx-badge--${type}">${label}</span>`,
      // Snippet completions co-located with this component.
      // No separate top-level 'completions' array needed.
      completions: [
        { label: 'badge-info',    kind: 'snip', body: '<Badge type="info"    label="$1" />' },
        { label: 'badge-success', kind: 'snip', body: '<Badge type="success" label="$1" />' },
        { label: 'badge-warning', kind: 'snip', body: '<Badge type="warning" label="$1" />' },
        { label: 'badge-error',   kind: 'snip', body: '<Badge type="error"   label="$1" />' },
      ],
    }),
  ],

  // Optional: block component with children
  // components: [
  //   defineComponent({
  //     tag: 'Callout',
  //     selfClosing: false,           // <Callout>...children...</Callout>
  //     attrs: {
  //       title: { type: 'string', default: 'Note' },
  //       type:  { type: 'enum', options: ['info','warning','danger'], default: 'info' },
  //     },
  //     render: ({ title, type, children }) =>
  //       `<div class="callout callout--${type}"><strong>${title}</strong>${children}</div>`,
  //   }),
  // ],

  styles: `
    .smdx-badge {
      display: inline-block;
      padding: 0.125rem 0.5rem;
      border-radius: 9999px;
      font-size: 0.75rem;
      font-weight: 600;
    }
    .smdx-badge--info    { background: #dbeafe; color: #1d4ed8; }
    .smdx-badge--success { background: #dcfce7; color: #15803d; }
    .smdx-badge--warning { background: #fef9c3; color: #a16207; }
    .smdx-badge--error   { background: #fee2e2; color: #b91c1c; }
  `,
};

export default badgePlugin;

MDX usage

Once the plugin is registered, authors write standard JSX in the editor:

content.mdx

<!-- Inline self-closing component -->
<Badge type="success" label="Stable" />
<Badge type="warning" label="Beta" />
<Badge type="error"   label="Deprecated" />

<!-- Block component with children (selfClosing: false) -->
<Callout title="Good to know" type="info">
  This feature is available from v2.0.0 onwards.
</Callout>
Raw Patterns

Pattern-based Plugin — patterns

Use patterns only when the syntax cannot be expressed as a JSX tag — e.g. fenced code blocks (```mermaid), directive fences (:::type), or math delimiters ($…$). The example above (my-plugin.ts) shows this approach end-to-end.

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 35 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".

patternsRendererConfig[]No

Raw regex-based renderers — escape hatch for non-JSX syntax (e.g. fenced code blocks, :::type admonitions, $...$ math). Prefer components for JSX-style MDX.

componentsRendererConfig[]No

Declarative MDX component definitions using defineComponent(). Syntax-highlight tokens and autocomplete completions are auto-registered for every listed tag. Preferred over patterns for JSX-style components.

parsersParserConfig[]No

Custom block parsers for extended MDX syntax.

completionsCompletionItem[]No

Static autocomplete items contributed by this plugin. Items with kind "snip" expand on Tab with $1 cursor position.

provideTokensPluginTokenProviderNo

Per-line syntax highlighter. Return TokenSegment[] to colour MDX syntax. Use componentTagTokens() for JSX components — handles tag names, attribute names/values, boolean attributes, and multi-line tags.

stylesstringNo

CSS string injected into the editor.

dependenciesstring[]No

Names of plugins that must be registered first.

SynclineMDX

© 2026 SynclineMDX Editor. All rights reserved.