Skip to main content
Plugins are the fundamental building blocks of Yoopta Editor. Everything in the editor - from simple paragraphs to complex accordions - is implemented as a plugin. This architecture provides maximum flexibility and extensibility.

What is a Plugin?

A plugin defines:
  • Elements: The structure and visual components
  • Options: Display information and shortcuts
  • Events: DOM event handlers (keyboard, mouse, etc.)
  • Lifecycle: Hooks for creation and destruction
  • Parsers: HTML/Markdown serialization and deserialization
  • Commands: Custom operations
  • Extensions: Slate editor customization
packages/core/editor/src/plugins/types.ts
type Plugin<TElementMap, TPluginOptions> = {
  type: string;
  elements: PluginElementsMap;
  options?: PluginOptions<TPluginOptions>;
  events?: PluginDOMEvents;
  lifecycle?: PluginLifeCycleEvents;
  parsers?: Partial<Record<PluginParserTypes, PluginParsers>>;
  commands?: Record<string, (editor: YooEditor, ...args: any[]) => any>;
  extensions?: (slate: SlateEditor, editor: YooEditor, blockId: string) => SlateEditor;
};

Creating a Plugin

Use the YooptaPlugin class to create plugins:
import { YooptaPlugin } from '@yoopta/editor';

const MyPlugin = new YooptaPlugin({
  type: 'MyPlugin',
  elements: {
    'my-element': {
      render: (props) => <div {...props.attributes}>{props.children}</div>,
      props: { nodeType: 'block' },
    }
  },
  options: {
    display: {
      title: 'My Plugin',
      description: 'A custom plugin',
    },
    shortcuts: ['my', 'custom'],
  },
});
Plugin type names should use PascalCase (“Paragraph”, “HeadingOne”) while element type names within plugins use kebab-case (“paragraph”, “heading-one”).

Plugin Elements

Elements define the structure and rendering of your plugin’s content:
packages/core/editor/src/plugins/types.ts
type PluginElement<TKeys, T> = {
  render?: (props: PluginElementRenderProps) => JSX.Element;
  props?: PluginElementProps<T>;
  asRoot?: boolean;                    // Is this the root element?
  children?: TKeys[];                  // Allowed child element types
  injectElementsFromPlugins?: string[];  // Allow elements from other plugins
  rootPlugin?: string;                 // Plugin this element belongs to
  placeholder?: string;                // Placeholder text when empty
};

Simple Element

const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: {
    paragraph: {
      render: (props) => <p {...props.attributes}>{props.children}</p>,
      props: { nodeType: 'block' },
    },
  },
});

Element with Custom Props

type CalloutProps = {
  nodeType: 'block';
  theme: 'info' | 'warning' | 'error' | 'success';
};

const Callout = new YooptaPlugin<{ callout: CalloutProps }>({
  type: 'Callout',
  elements: {
    callout: {
      render: (props) => {
        const { theme } = props.element.props || {};
        return (
          <div 
            className={`callout callout-${theme}`}
            {...props.attributes}
          >
            {props.children}
          </div>
        );
      },
      props: { 
        nodeType: 'block',
        theme: 'info' // default value
      },
    },
  },
});

Nested Elements

Plugins can have multiple element types with hierarchical structure:
const Accordion = new YooptaPlugin({
  type: 'Accordion',
  elements: {
    'accordion-list': {
      render: (props) => <div {...props.attributes}>{props.children}</div>,
      props: { nodeType: 'block' },
      asRoot: true,
      children: ['accordion-list-item'],
    },
    'accordion-list-item': {
      render: (props) => (
        <details {...props.attributes}>
          {props.children}
        </details>
      ),
      props: { nodeType: 'block', isExpanded: false },
      children: ['accordion-list-item-heading', 'accordion-list-item-content'],
    },
    'accordion-list-item-heading': {
      render: (props) => <summary {...props.attributes}>{props.children}</summary>,
      props: { nodeType: 'block' },
    },
    'accordion-list-item-content': {
      render: (props) => <div {...props.attributes}>{props.children}</div>,
      props: { nodeType: 'block' },
      // Allow nesting other plugins
      injectElementsFromPlugins: ['Paragraph', 'HeadingOne', 'HeadingTwo'],
    },
  },
});
Use injectElementsFromPlugins to allow elements from other plugins to be nested within your element. This is powerful for container-like plugins such as accordions, tabs, or callouts.

Void Elements

Void elements don’t contain editable content:
const Divider = new YooptaPlugin({
  type: 'Divider',
  elements: {
    divider: {
      render: (props) => (
        <div {...props.attributes} contentEditable={false}>
          <hr />
          {props.children} {/* Required empty text node */}
        </div>
      ),
      props: { nodeType: 'void' },
    },
  },
});

Inline Elements

Inline elements flow within text:
const Link = new YooptaPlugin({
  type: 'Link',
  elements: {
    link: {
      render: (props) => (
        <a 
          href={props.element.props.url}
          target={props.element.props.target}
          {...props.attributes}
        >
          {props.children}
        </a>
      ),
      props: { 
        nodeType: 'inline',
        url: '',
        target: '_blank'
      },
    },
  },
});

Plugin Options

Options provide metadata and configuration:
packages/core/editor/src/plugins/types.ts
type PluginOptions<T> = Partial<{
  display?: {
    title?: string;        // Display name
    description?: string;  // Description
    icon?: ReactNode;      // Icon component
  };
  shortcuts?: string[];    // Slash command shortcuts
  HTMLAttributes?: HTMLAttributes<HTMLElement>;
} & T>;
const HeadingOne = new YooptaPlugin({
  type: 'HeadingOne',
  elements: { /* ... */ },
  options: {
    display: {
      title: 'Heading 1',
      description: 'Large section heading',
      icon: <Heading1Icon />,
    },
    shortcuts: ['h1', 'heading1', '#'],
  },
});
These options are used by UI components like slash command menus.

Plugin Events

Handle DOM events at the plugin level:
packages/core/editor/src/plugins/types.ts
type PluginDOMEvents = {
  [key in keyof EditorEventHandlers]: (
    editor: YooEditor,
    slate: SlateEditor,
    options: PluginEventHandlerOptions,
  ) => EditorEventHandlers[key] | void;
};

Keyboard Events

const MyPlugin = new YooptaPlugin({
  type: 'MyPlugin',
  elements: { /* ... */ },
  events: {
    onKeyDown: (editor, slate, options) => (event) => {
      const { hotkeys, currentBlock } = options;
      
      // Handle Enter key
      if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        editor.splitBlock();
        return;
      }
      
      // Handle backspace at start
      if (event.key === 'Backspace') {
        const { selection } = slate;
        if (selection && Range.isCollapsed(selection)) {
          const [start] = Range.edges(selection);
          if (start.offset === 0 && start.path[0] === 0) {
            event.preventDefault();
            editor.toggleBlock(currentBlock.id, { type: 'Paragraph' });
            return;
          }
        }
      }
    },
  },
});

Available Event Handlers

  • onKeyDown - Keyboard press
  • onKeyUp - Keyboard release
  • onClick - Mouse click
  • onMouseDown - Mouse button press
  • onMouseUp - Mouse button release
  • onFocus - Element receives focus
  • onBlur - Element loses focus
  • onPaste - Paste event
  • onCopy - Copy event
  • onCut - Cut event

Plugin Lifecycle

Hook into block creation and destruction:
packages/core/editor/src/plugins/types.ts
type PluginLifeCycleEvents = {
  beforeCreate?: (editor: YooEditor) => SlateElement;
  onCreate?: (editor: YooEditor, blockId: string) => void;
  onDestroy?: (editor: YooEditor, blockId: string) => void;
};
const MyPlugin = new YooptaPlugin({
  type: 'MyPlugin',
  elements: { /* ... */ },
  lifecycle: {
    beforeCreate: (editor) => {
      // Return custom initial structure
      return editor.y('my-element', {
        props: { customProp: 'initial value' },
        children: [editor.y.text('Default content')]
      });
    },
    onCreate: (editor, blockId) => {
      console.log('Block created:', blockId);
      // Initialize external resources
    },
    onDestroy: (editor, blockId) => {
      console.log('Block destroyed:', blockId);
      // Cleanup external resources
    },
  },
});

Plugin Parsers

Handle serialization and deserialization:
packages/core/editor/src/plugins/types.ts
type PluginParsers = {
  deserialize?: PluginDeserializeParser;
  serialize?: PluginSerializeParser;
};

type PluginDeserializeParser = {
  nodeNames: string[];  // HTML tag names to parse
  parse?: (el: HTMLElement, editor: YooEditor) => SlateElement | YooptaBlockData[] | void;
};

type PluginSerializeParser = (
  element: SlateElement,
  content: string,
  blockMetaData?: YooptaBlockBaseMeta,
) => string;

HTML Parser

packages/plugins/paragraph/src/plugin/paragraph-plugin.tsx
const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: { /* ... */ },
  parsers: {
    html: {
      deserialize: {
        nodeNames: ['P'],  // Parse <p> tags
      },
      serialize: (element, text, blockMeta) => {
        const { align = 'left', depth = 0 } = blockMeta || {};
        return `<p 
          data-meta-align="${align}" 
          data-meta-depth="${depth}" 
          style="margin-left: ${depth * 20}px; text-align: ${align}"
        >${serializeTextNodes(element.children)}</p>`;
      },
    },
  },
});

Markdown Parser

packages/plugins/paragraph/src/plugin/paragraph-plugin.tsx
const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: { /* ... */ },
  parsers: {
    markdown: {
      serialize: (element) => 
        `${serializeTextNodesIntoMarkdown(element.children)}\n`,
    },
  },
});

Email Parser

const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: { /* ... */ },
  parsers: {
    email: {
      serialize: (element, text, blockMeta) => {
        const { align = 'left', depth = 0 } = blockMeta || {};
        return `<table style="width: 100%">
          <tbody>
            <tr>
              <td>
                <p style="font-size: 16px; line-height: 1.75rem; margin: .5rem 0 0">
                  ${serializeTextNodes(element.children)}
                </p>
              </td>
            </tr>
          </tbody>
        </table>`;
      },
    },
  },
});
The serializeTextNodes and serializeTextNodesIntoMarkdown helper functions handle text formatting (bold, italic, etc.) automatically.

Plugin Commands

Add custom methods to the editor:
const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: { /* ... */ },
  commands: {
    convertToParagraph: (editor, blockId: string) => {
      editor.toggleBlock(blockId, { type: 'Paragraph' });
    },
    insertParagraphAfter: (editor, blockId: string) => {
      const block = editor.getBlock({ id: blockId });
      if (!block) return;
      
      editor.insertBlock('Paragraph', {
        at: block.meta.order + 1,
        focus: true,
      });
    },
  },
});

// Use commands
editor.plugins.Paragraph.commands.convertToParagraph(editor, blockId);

Plugin Extensions

Customize the Slate editor for your plugin:
const withMyPlugin = (slate: SlateEditor, editor: YooEditor, blockId: string) => {
  const { insertText, insertBreak, normalizeNode } = slate;
  
  // Override insertText
  slate.insertText = (text) => {
    // Custom text insertion logic
    insertText(text);
  };
  
  // Override insertBreak
  slate.insertBreak = () => {
    // Custom break behavior
    editor.splitBlock();
  };
  
  // Override normalizeNode
  slate.normalizeNode = (entry) => {
    // Custom normalization
    normalizeNode(entry);
  };
  
  return slate;
};

const MyPlugin = new YooptaPlugin({
  type: 'MyPlugin',
  elements: { /* ... */ },
  extensions: withMyPlugin,
});
Extensions modify Slate editor behavior at a low level. Use them carefully and ensure you don’t break core functionality.

Extending Plugins

You can extend existing plugins to customize them:
packages/core/editor/src/plugins/create-yoopta-plugin.tsx
const CustomParagraph = Paragraph.extend({
  options: {
    display: {
      title: 'Custom Paragraph',
      description: 'A customized paragraph',
    },
  },
  elements: {
    paragraph: {
      render: (props) => (
        <p className="custom-paragraph" {...props.attributes}>
          {props.children}
        </p>
      ),
    },
  },
});

Extend Options

type ExtendPlugin<TElementMap, TOptions> = {
  options?: Partial<PluginOptions<TOptions>>;
  events?: Partial<PluginDOMEvents>;
  lifecycle?: Partial<PluginLifeCycleEvents>;
  injectElementsFromPlugins?: YooptaPlugin<any, any>[];
  elements?: {
    [K in keyof TElementMap]?: {
      render?: (props: PluginElementRenderProps) => JSX.Element;
      props?: Record<string, unknown>;
      injectElementsFromPlugins?: YooptaPlugin<any, any>[];
      placeholder?: string;
    };
  };
};

Injecting Elements

Allow other plugin elements to be nested:
import { Paragraph, HeadingOne, HeadingTwo, Image } from '@yoopta/plugins';

const CustomAccordion = Accordion.extend({
  injectElementsFromPlugins: [
    Paragraph,
    HeadingOne,
    HeadingTwo,
    Image,
  ],
});
Now accordions can contain paragraphs, headings, and images.

Using Plugins

Register plugins when creating the editor:
import { createYooptaEditor } from '@yoopta/editor';
import { Paragraph, HeadingOne, HeadingTwo } from '@yoopta/plugins';

const PLUGINS = [Paragraph, HeadingOne, HeadingTwo];

const editor = useMemo(() => createYooptaEditor({
  plugins: PLUGINS,
  marks: MARKS,
}), []);

Real-World Example

Here’s a complete plugin example:
import { YooptaPlugin, serializeTextNodes } from '@yoopta/editor';

type QuoteElementMap = {
  quote: {
    type: 'quote';
    props: {
      nodeType: 'block';
      author?: string;
      source?: string;
    };
  };
};

const Quote = new YooptaPlugin<QuoteElementMap>({
  type: 'Quote',
  elements: {
    quote: {
      render: (props) => {
        const { author, source } = props.element.props || {};
        return (
          <blockquote {...props.attributes}>
            <div className="quote-content">{props.children}</div>
            {(author || source) && (
              <footer className="quote-footer">
                {author && <cite>{author}</cite>}
                {source && <span> from {source}</span>}
              </footer>
            )}
          </blockquote>
        );
      },
      props: {
        nodeType: 'block',
      },
      placeholder: 'Enter quote...',
    },
  },
  options: {
    display: {
      title: 'Quote',
      description: 'Insert a quotation',
      icon: <QuoteIcon />,
    },
    shortcuts: ['quote', 'blockquote', '>'],
  },
  parsers: {
    html: {
      deserialize: {
        nodeNames: ['BLOCKQUOTE'],
      },
      serialize: (element, text) => {
        const { author, source } = element.props || {};
        let html = `<blockquote>${serializeTextNodes(element.children)}`;
        if (author || source) {
          html += '<footer>';
          if (author) html += `<cite>${author}</cite>`;
          if (source) html += ` from ${source}`;
          html += '</footer>';
        }
        html += '</blockquote>';
        return html;
      },
    },
    markdown: {
      serialize: (element) => `> ${serializeTextNodesIntoMarkdown(element.children)}\n`,
    },
  },
  commands: {
    setAuthor: (editor, blockId: string, author: string) => {
      const block = editor.getBlock({ id: blockId });
      if (!block) return;
      
      editor.updateElement({
        blockId,
        elementId: block.value[0].id,
        props: { author },
      });
    },
  },
});

export { Quote };

Best Practices

1. Type Your Plugin

Define proper TypeScript types for your element map:
type MyElementMap = {
  'my-element': {
    type: 'my-element';
    props: {
      nodeType: 'block';
      customProp: string;
    };
  };
};

const MyPlugin = new YooptaPlugin<MyElementMap>({ /* ... */ });

2. Use Semantic HTML

Render with appropriate HTML elements:
// Good
render: (props) => <h1 {...props.attributes}>{props.children}</h1>

// Bad
render: (props) => <div className="heading" {...props.attributes}>{props.children}</div>

3. Handle Edge Cases

Always handle empty content, missing props, etc.:
render: (props) => {
  const { url, alt } = props.element.props || {};
  if (!url) return <div {...props.attributes}>{props.children}</div>;
  
  return (
    <img src={url} alt={alt || ''} {...props.attributes} />
  );
}

4. Provide Shortcuts

Make plugins discoverable with intuitive shortcuts:
options: {
  shortcuts: ['h1', 'heading1', '#']  // Multiple aliases
}

5. Add Placeholders

Guide users with helpful placeholder text:
elements: {
  'my-element': {
    placeholder: 'Type something here...',
  }
}

Next Steps

Themes

Style your plugins with themes

Elements

Understand element structures

Events

Handle editor events

Built-in Plugins

Explore available plugins

Build docs developers (and LLMs) love